diff --git a/.gitattributes b/.gitattributes index b926727db..78e510d78 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,5 @@ # Force this repository to use Windows line endings -* text=crlf +* text=auto # Custom for Visual Studio *.ascx text @@ -61,3 +61,8 @@ BlitzBuilder/ export-ignore sp_AskBrent/ export-ignore sp_Blitz/ export-ignore sp_BlitzIndex/ export-ignore + +# linguist overrides +*.sql linguist-language=TSQL +*.R linguist-language=R +*.ps1 linguist-language=Powershell diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..e6d4e32a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug report +about: Create a report to help us improve + +--- + +**Version of the script** +Look in the stored procedure, and it'll have a version date & number near the top. Put that in here. If it's not the current version (dated in the last month), then upgrade to the current version and test that before reporting a bug - we fix a lot of stuff in each new build. We'll flat out close bug reports for older builds. + +**What is the current behavior?** + +**If the current behavior is a bug, please provide the steps to reproduce.** + +**What is the expected behavior?** + +**Which versions of SQL Server and which OS are affected by this issue? Did this work in previous versions of our procedures?** diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..46b4c4fa2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Are you ready to build the code for the feature?** +As much as we'd love to build everything that everyone wants for free, we need your help. Open source is built with your help and code. Are you ready to commit time to this project? Have you got existing code you can help contribute to solve the problem? diff --git a/.github/ISSUE_TEMPLATE.md b/.github/bug_report.md similarity index 55% rename from .github/ISSUE_TEMPLATE.md rename to .github/bug_report.md index 4df185e07..cfaa64e59 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/bug_report.md @@ -1,5 +1,11 @@ -**Do you want to request a *feature* or report a *bug*?** -If you're reporting a bug, include the version number of the script you're using. If it's not the current version, upgrade to the current version and test that before reporting a bug - we fix a lot of stuff in each new build. +--- +name: Bug report +about: Create a bug report to help us improve + +--- + +Include the version number of the script you're using. +If it's not the current version, upgrade to the current version and test that before reporting a bug - we fix a lot of stuff in each new build. **What is the current behavior?** diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/feature_request.md similarity index 73% rename from .github/PULL_REQUEST_TEMPLATE.md rename to .github/feature_request.md index deee0696d..257aee586 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/feature_request.md @@ -1,14 +1,16 @@ +--- +name: Feature request +about: Suggest an idea for this project + +--- + Fixes # . Changes proposed in this pull request: - - - - - How to test this code: - - - - - Has been tested on (remove any that don't apply): - Case-sensitive SQL Server instance @@ -17,5 +19,7 @@ Has been tested on (remove any that don't apply): - SQL Server 2012 - SQL Server 2014 - SQL Server 2016 + - SQL Server 2017 + - SQL Server 2019 - Amazon RDS - Azure SQL DB diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 000000000..37b1a5870 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,57 @@ +name: First Responder Kit Integration Tests + +on: + push: + workflow_dispatch: + pull_request: + types: [opened, review_requested, synchronize] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + strategy: + matrix: + SQL_ENGINE_VERSION: [2017,2019,2022] + COLLATION: [SQL_Latin1_General_CP1_CS_AS] + + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install SqlServer Module + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module SqlServer + + - name: Install SQL Server + uses: potatoqualitee/mssqlsuite@v1.7 + with: + install: sqlengine + version: ${{ matrix.SQL_ENGINE_VERSION }} + collation: ${{ matrix.COLLATION }} + + - name: Check SQL Install + run: | + sqlcmd -S localhost -U sa -P dbatools.I0 -d tempdb -Q "SELECT @@version as Version;" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d tempdb -Q "SELECT SERVERPROPERTY('Collation') AS Collation;" -I -b -t 60 + + - name: Deploy FRK + run: | + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzCache.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzWho.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_Blitz.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzFirst.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzAnalysis.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzBackups.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzIndex.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -i "sp_BlitzLock.sql" -I -b -t 60 + sqlcmd -S localhost -U sa -P dbatools.I0 -d master -Q "SELECT * FROM sys.procedures WHERE name LIKE 'sp_Blitz%';" -I -b -t 60 + + - name: Run Pester Tests + shell: pwsh + run: | + cd tests + ./run-tests.ps1 diff --git a/.gitignore b/.gitignore index d959d3a6c..c4639039a 100644 --- a/.gitignore +++ b/.gitignore @@ -131,3 +131,4 @@ test-results/* x64/ ~$* ~*.* +/.vs diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..7636d430c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at Help@BrentOzar.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba8c6a876..8ed84478c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,7 +22,7 @@ If you can't find a similar issue, go ahead and open your own. Include as much d Open source is community-built software. Anyone is welcome to build things that would help make their job easier. -Open source isn't free development, though. Working on these scripts is hard work: they have to work on case-sensitive instances, and on all supported versions of SQL Server (currently 2008 through 2017.) If you just waltz in and say, "Someone please bake me a cake," you're probably not going to get a cake. +Open source isn't free development, though. Working on these scripts is hard work: they have to work on case-sensitive instances, and on all versions of SQL Server that Microsoft currently supports. If you just waltz in and say, "Someone please bake me a cake," you're probably not going to get a cake. If you want something, you're going to either need to build it yourself, or convince someone else to devote their free time to your feature request. You can do that by sponsoring development (offering to hire a developer to build it for you), or getting people excited enough that they volunteer to build it for you. @@ -38,63 +38,30 @@ After your Github issue has gotten good responses from a couple of volunteers wh Note that if you're not ready to get started coding in the next week, or if you think you can't finish the feature in the next 30 days, you probably don't want to bother opening an issue. You're only going to feel guilty over not making progress, because we'll keep checking in with you to see how it's going. We don't want to have stale "someday I'll build that" issues in the list - we want to keep the open issues list easy to scan for folks who are trying to troubleshoot bugs and feature requests. -### Contributing Changes to Power BI +### Code Requirements and Standards -Power BI files are binary files that don't work well with Git source control. Rather than sending someone your changed Power BI files, here's what you need to do: +We're not picky at all about style, but a few things to know: -1. Make the changes on your side, test them, and make sure they work. -2. In the Github issue you created above (in the How to Build Features section), add step-by-step instructions for someone else to make the same change to the master Power BI files. -3. The First Responder Kit maintainers will review your changes and try to reproduce your results with the same steps. If they produce the right results, congratulations! They'll be saved permanently. +Don't touch the files that start with Install, like Install-All-Scripts.sql. Those are dynamically generated. You only have to touch the ones that start with sp_. -Why not just email your file to the maintainers? Well, lots of folks may be working on slightly different changes at the same time, and we need to be able to fold everyone's changes together at different points in time. +Your code needs to compile & run on all currently supported versions of SQL Server. It's okay if functionality degrades, like if not all features are available, but at minimum the code has to compile and run. -### Contributing T-SQL Code: Git Flow for Pull Requests - +Your code must handle: -1. [Fork] the project, clone your fork, and configure the remotes: +* Case sensitive databases & servers +* Unicode object names (databases, tables, indexes, etc.) +* Different date formats - for guidance: https://xkcd.com/1179/ - ```bash - # Clone your fork of the repo into the current directory - git clone git@github.com:/SQL-Server-First-Responder-Kit.git - # Navigate to the newly cloned directory - cd SQL-Server-First-Responder-Kit - # Assign the original repo to a remote called "upstream" - git remote add upstream https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - ``` +We know that's a pain, but that's the kind of thing we find out in the wild. Of course you would never build a server like that, but... -2. If you cloned a while ago, get the latest changes from upstream: +### How to Check In Your Code - ```bash - git checkout master - git pull upstream master - ``` +Rather than give you step-by-step instructions here, we'd rather link you to the work of others: -3. Create a new topic branch (off the main project development branch) to - contain your feature, change, or fix: +* [How to fork a GitHub repository and contribute to an open source project](https://blog.robsewell.com/blog/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/) - ```bash - git checkout -b - ``` -4. Commit your changes in logical chunks. Please adhere to these [git commit message guidelines] - or your code is unlikely be merged into the main project. Use Git's [interactive rebase] - feature to tidy up your commits before making them public. -5. Locally merge (or rebase) the upstream development branch into your topic branch: - - ```bash - git pull [--rebase] upstream master - ``` - -6. Push your topic branch up to your fork: - - ```bash - git push origin - ``` - -7. [Open a Pull Request] with a clear title and description. - -**IMPORTANT**: By submitting a patch, you agree to allow the project owner to license your work under the MIT [LICENSE] ## The Contributor Covenant Code of Conduct @@ -151,9 +118,4 @@ available at [http://contributor-covenant.org/version/1/4][version] [version]: http://contributor-covenant.org/version/1/4/ [Github issues list]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues [closed issues list]: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues?q=is%3Aissue+is%3Aclosed -[Fork]:https://help.github.com/articles/fork-a-repo/ -[git commit message guidelines]:http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html -[interactive rebase]:https://help.github.com/articles/about-git-rebase/ -[Open a Pull Request]:https://help.github.com/articles/about-pull-requests/ -[LICENSE]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/master/LICENSE.md [download the dev branch version]: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/archive/dev.zip diff --git a/Deprecated/readme.txt b/Deprecated/readme.txt new file mode 100644 index 000000000..1e4e774fb --- /dev/null +++ b/Deprecated/readme.txt @@ -0,0 +1,3 @@ +sp_AllNightLog, sp_AllNightLog_Setup, sp_BlitzInMemoryOLTP, and sp_BlitzQueryStore are no longer maintained. They may still work, but no guarantees. Please don't submit issues or pull requests to change them. You're welcome to fork the code and build your own supported version, of course, since they're open source. + +The other files in this folder are older versions of the First Responder Kit that work with versions of Microsoft SQL Server that Microsoft themselves no longer support. If you're cursed enough to work with antiques like SQL Server 2012, 2008, or heaven forbid, 2005, you may still be able to get value out of these, but we don't support the scripts anymore either. \ No newline at end of file diff --git a/sp_AllNightLog.sql b/Deprecated/sp_AllNightLog.sql similarity index 91% rename from sp_AllNightLog.sql rename to Deprecated/sp_AllNightLog.sql index 506c95c4e..e886de70d 100644 --- a/sp_AllNightLog.sql +++ b/Deprecated/sp_AllNightLog.sql @@ -20,16 +20,23 @@ ALTER PROCEDURE dbo.sp_AllNightLog @Restore BIT = 0, @Debug BIT = 0, @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; +SET STATISTICS XML OFF; BEGIN; -DECLARE @Version VARCHAR(30); -SET @Version = '2.0'; -SET @VersionDate = '20171201'; + +SELECT @Version = '8.19', @VersionDate = '20240222'; + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; IF @Help = 1 @@ -76,7 +83,7 @@ BEGIN MIT License - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -117,6 +124,7 @@ DECLARE @servercertificate NVARCHAR(MAX); --Config table: server certificate tha DECLARE @restore_path_base NVARCHAR(MAX); --Used to hold the base backup path in our configuration table DECLARE @restore_path_full NVARCHAR(MAX); --Used to hold the full backup path in our configuration table DECLARE @restore_path_log NVARCHAR(MAX); --Used to hold the log backup path in our configuration table +DECLARE @restore_move_files INT; -- used to hold the move files bit in our configuration table DECLARE @db_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL to create msdbCentral DECLARE @tbl_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL that creates tables in msdbCentral DECLARE @database_name NVARCHAR(256) = N'msdbCentral'; --Used to hold the name of the database we create to centralize data @@ -158,21 +166,28 @@ IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND v END /* -Make sure Ola Hallengren's scripts are installed in master +Make sure Ola Hallengren's scripts are installed in same database */ -IF 2 <> (SELECT COUNT(*) FROM master.sys.procedures WHERE name IN('CommandExecute', 'DatabaseBackup')) +DECLARE @CurrentDatabaseContext nvarchar(128) = DB_NAME(); +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'CommandExecute') BEGIN - RAISERROR('Ola Hallengren''s CommandExecute and DatabaseBackup must be installed in the master database. More info: http://ola.hallengren.com', 0, 1) WITH NOWAIT + RAISERROR('Ola Hallengren''s CommandExecute must be installed in the same database (%s) as SQL Server First Responder Kit. More info: http://ola.hallengren.com', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; + + RETURN; + END +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'DatabaseBackup') + BEGIN + RAISERROR('Ola Hallengren''s DatabaseBackup must be installed in the same database (%s) as SQL Server First Responder Kit. More info: http://ola.hallengren.com', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; RETURN; END /* -Make sure sp_DatabaseRestore is installed in master +Make sure sp_DatabaseRestore is installed in same database */ -IF NOT EXISTS (SELECT * FROM master.sys.procedures WHERE name = 'sp_DatabaseRestore') +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'sp_DatabaseRestore') BEGIN - RAISERROR('sp_DatabaseRestore must be installed in master. To get it: http://FirstResponderKit.org', 0, 1) WITH NOWAIT + RAISERROR('sp_DatabaseRestore must be installed in the same database (%s) as SQL Server First Responder Kit. To get it: http://FirstResponderKit.org', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; RETURN; END @@ -229,7 +244,17 @@ IF (@PollDiskForNewDatabases = 1 OR @Restore = 1) AND OBJECT_ID('msdb.dbo.restor END; END /* IF CHARINDEX('**', @restore_path_base) <> 0 */ - + + SELECT @restore_move_files = CONVERT(BIT, configuration_setting) + FROM msdb.dbo.restore_configuration c + WHERE configuration_name = N'move files'; + + IF @restore_move_files is NULL + BEGIN + -- Set to default value of 1 + SET @restore_move_files = 1 + END + END /* IF @PollDiskForNewDatabases = 1 OR @Restore = 1 */ @@ -343,7 +368,7 @@ Pollster: SELECT 1 FROM msdbCentral.dbo.backup_worker bw WITH (READPAST) WHERE bw.last_log_backup_finish_time < '99991231' - AND bw.last_log_backup_start_time < DATEADD(MINUTE, -5, GETDATE()) + AND bw.last_log_backup_start_time < DATEADD(SECOND, (@rpo * -1), GETDATE()) AND EXISTS ( SELECT 1 FROM msdb.dbo.backupset b @@ -363,7 +388,7 @@ Pollster: bw.last_log_backup_start_time = '19000101' FROM msdbCentral.dbo.backup_worker bw WHERE bw.last_log_backup_finish_time < '99991231' - AND bw.last_log_backup_start_time < DATEADD(MINUTE, -5, GETDATE()) + AND bw.last_log_backup_start_time < DATEADD(SECOND, (@rpo * -1), GETDATE()) AND EXISTS ( SELECT 1 FROM msdb.dbo.backupset b @@ -553,6 +578,7 @@ DiskPollster: SELECT fl.BackupFile FROM @FileList AS fl WHERE fl.BackupFile IS NOT NULL + AND fl.BackupFile COLLATE DATABASE_DEFAULT NOT IN (SELECT name from sys.databases where database_id < 5) AND NOT EXISTS ( SELECT 1 @@ -576,7 +602,7 @@ DiskPollster: SELECT 1 FROM msdb.dbo.restore_worker rw WITH (READPAST) WHERE rw.last_log_restore_finish_time < '99991231' - AND rw.last_log_restore_start_time < DATEADD(MINUTE, -5, GETDATE()) + AND rw.last_log_restore_start_time < DATEADD(SECOND, (@rto * -1), GETDATE()) AND EXISTS ( SELECT 1 FROM msdb.dbo.restorehistory r @@ -596,7 +622,7 @@ DiskPollster: rw.last_log_restore_start_time = '19000101' FROM msdb.dbo.restore_worker rw WHERE rw.last_log_restore_finish_time < '99991231' - AND rw.last_log_restore_start_time < DATEADD(MINUTE, -5, GETDATE()) + AND rw.last_log_restore_start_time < DATEADD(SECOND, (@rto * -1), GETDATE()) AND EXISTS ( SELECT 1 FROM msdb.dbo.restorehistory r @@ -797,6 +823,7 @@ LogShamer: AND bw.is_completed = 1 AND bw.last_log_backup_start_time < DATEADD(SECOND, (@rpo * -1), GETDATE()) AND (bw.error_number IS NULL OR bw.error_number > 0) /* negative numbers indicate human attention required */ + AND bw.ignore_database = 0 ) OR ( /*This section picks up newly added databases by Pollster*/ @@ -805,6 +832,7 @@ LogShamer: AND bw.last_log_backup_start_time = '1900-01-01 00:00:00.000' AND bw.last_log_backup_finish_time = '9999-12-31 00:00:00.000' AND (bw.error_number IS NULL OR bw.error_number > 0) /* negative numbers indicate human attention required */ + AND bw.ignore_database = 0 ) ORDER BY bw.last_log_backup_start_time ASC, bw.last_log_backup_finish_time ASC, bw.database_name ASC; @@ -902,7 +930,7 @@ LogShamer: */ IF @encrypt = 'Y' - EXEC master.dbo.DatabaseBackup @Databases = @database, --Database we're working on + EXEC dbo.DatabaseBackup @Databases = @database, --Database we're working on @BackupType = 'LOG', --Going for the LOGs @Directory = @backup_path, --The path we need to back up to @Verify = 'N', --We don't want to verify these, it eats into job time @@ -915,7 +943,7 @@ LogShamer: @ServerCertificate = @servercertificate; ELSE - EXEC master.dbo.DatabaseBackup @Databases = @database, --Database we're working on + EXEC dbo.DatabaseBackup @Databases = @database, --Database we're working on @BackupType = 'LOG', --Going for the LOGs @Directory = @backup_path, --The path we need to back up to @Verify = 'N', --We don't want to verify these, it eats into job time @@ -1158,21 +1186,31 @@ IF @Restore = 1 ELSE 0 END FROM msdb.dbo.restore_worker rw WITH (UPDLOCK, HOLDLOCK, ROWLOCK) - WHERE - ( /*This section works on databases already part of the backup cycle*/ - rw.is_started = 0 - AND rw.is_completed = 1 - AND rw.last_log_restore_start_time < DATEADD(SECOND, (@rto * -1), GETDATE()) - AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ - ) - OR - ( /*This section picks up newly added databases by DiskPollster*/ - rw.is_started = 0 - AND rw.is_completed = 0 - AND rw.last_log_restore_start_time = '1900-01-01 00:00:00.000' - AND rw.last_log_restore_finish_time = '9999-12-31 00:00:00.000' - AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ - ) + WHERE ( + ( /*This section works on databases already part of the backup cycle*/ + rw.is_started = 0 + AND rw.is_completed = 1 + AND rw.last_log_restore_start_time < DATEADD(SECOND, (@rto * -1), GETDATE()) + AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ + ) + OR + ( /*This section picks up newly added databases by DiskPollster*/ + rw.is_started = 0 + AND rw.is_completed = 0 + AND rw.last_log_restore_start_time = '1900-01-01 00:00:00.000' + AND rw.last_log_restore_finish_time = '9999-12-31 00:00:00.000' + AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ + ) + ) + AND rw.ignore_database = 0 + AND NOT EXISTS ( + /* Validation check to ensure the database either doesn't exist or is in a restoring/standby state */ + SELECT 1 + FROM sys.databases d + WHERE d.name = rw.database_name + AND state <> 1 /* Restoring */ + AND NOT (state=0 AND d.is_in_standby=1) /* standby mode */ + ) ORDER BY rw.last_log_restore_start_time ASC, rw.last_log_restore_finish_time ASC, rw.database_name ASC; @@ -1300,12 +1338,13 @@ IF @Restore = 1 IF @Debug = 1 RAISERROR('Starting Log only restores', 0, 1) WITH NOWAIT; - EXEC master.dbo.sp_DatabaseRestore @Database = @database, + EXEC dbo.sp_DatabaseRestore @Database = @database, @BackupPathFull = @restore_path_full, @BackupPathLog = @restore_path_log, @ContinueLogs = 1, @RunRecovery = 0, @OnlyLogsAfter = @only_logs_after, + @MoveFiles = @restore_move_files, @Debug = @Debug END @@ -1317,11 +1356,12 @@ IF @Restore = 1 IF @Debug = 1 RAISERROR('Starting first Full restore from: ', 0, 1) WITH NOWAIT; IF @Debug = 1 RAISERROR(@restore_path_full, 0, 1) WITH NOWAIT; - EXEC master.dbo.sp_DatabaseRestore @Database = @database, + EXEC dbo.sp_DatabaseRestore @Database = @database, @BackupPathFull = @restore_path_full, @BackupPathLog = @restore_path_log, @ContinueLogs = 0, @RunRecovery = 0, + @MoveFiles = @restore_move_files, @Debug = @Debug END @@ -1475,4 +1515,4 @@ RETURN; END; -- Final END for stored proc -GO \ No newline at end of file +GO diff --git a/sp_AllNightLog_Setup.sql b/Deprecated/sp_AllNightLog_Setup.sql similarity index 95% rename from sp_AllNightLog_Setup.sql rename to Deprecated/sp_AllNightLog_Setup.sql index 3c33085c5..0925414d2 100644 --- a/sp_AllNightLog_Setup.sql +++ b/Deprecated/sp_AllNightLog_Setup.sql @@ -26,18 +26,24 @@ ALTER PROCEDURE dbo.sp_AllNightLog_Setup @Debug BIT = 0, @FirstFullBackup BIT = 0, @FirstDiffBackup BIT = 0, + @MoveFiles BIT = 1, @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; +SET STATISTICS XML OFF; BEGIN; -DECLARE @Version VARCHAR(30); -SET @Version = '2.0'; -SET @VersionDate = '20171201'; +SELECT @Version = '8.19', @VersionDate = '20240222'; +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; IF @Help = 1 @@ -67,6 +73,7 @@ BEGIN * Holds variables used by stored proc to make runtime decisions * RTO: Seconds, how often to look for log backups to restore * Restore Path: The path we feed to sp_DatabaseRestore + * Move Files: Whether to move files to default data/log directories. * dbo.restore_worker * Holds list of databases and some information that helps our Agent jobs figure out if they need to look for files to restore @@ -96,7 +103,8 @@ BEGIN @RunSetup BIT, defaults to 0. When this is set to 1, it will run the setup portion to create database, tables, and worker jobs. @UpdateSetup BIT, defaults to 0. When set to 1, will update existing configs for RPO/RTO and database backup/restore paths. @RPOSeconds BIGINT, defaults to 30. Value in seconds you want to use to determine if a new log backup needs to be taken. - @BackupPath NVARCHAR(MAX), defaults to = ''D:\Backup''. You 99.99999% will need to change this path to something else. This tells Ola''s job where to put backups. + @BackupPath NVARCHAR(MAX), This is REQUIRED if @Runsetup=1. This tells Ola''s job where to put backups. + @MoveFiles BIT, defaults to 1. When this is set to 1, it will move files to default data/log directories @Debug BIT, defaults to 0. Whent this is set to 1, it prints out dynamic SQL commands Sample call: @@ -111,7 +119,7 @@ BEGIN MIT License - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -259,21 +267,28 @@ IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND v END /* -Make sure Ola Hallengren's scripts are installed in master +Make sure Ola Hallengren's scripts are installed in same database */ -IF 2 <> (SELECT COUNT(*) FROM master.sys.procedures WHERE name IN('CommandExecute', 'DatabaseBackup')) +DECLARE @CurrentDatabaseContext nvarchar(128) = DB_NAME(); +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'CommandExecute') + BEGIN + RAISERROR('Ola Hallengren''s CommandExecute must be installed in the same database (%s) as SQL Server First Responder Kit. More info: http://ola.hallengren.com', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; + + RETURN; + END +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'DatabaseBackup') BEGIN - RAISERROR('Ola Hallengren''s CommandExecute and DatabaseBackup must be installed in the master database. More info: http://ola.hallengren.com', 0, 1) WITH NOWAIT + RAISERROR('Ola Hallengren''s DatabaseBackup must be installed in the same database (%s) as SQL Server First Responder Kit. More info: http://ola.hallengren.com', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; RETURN; END /* -Make sure sp_DatabaseRestore is installed in master +Make sure sp_DatabaseRestore is installed in same database */ -IF NOT EXISTS (SELECT * FROM master.sys.procedures WHERE name = 'sp_DatabaseRestore') +IF NOT EXISTS (SELECT * FROM sys.procedures WHERE name = 'sp_DatabaseRestore') BEGIN - RAISERROR('sp_DatabaseRestore must be installed in master. To get it: http://FirstResponderKit.org', 0, 1) WITH NOWAIT + RAISERROR('sp_DatabaseRestore must be installed in the same database (%s) as SQL Server First Responder Kit. To get it: http://FirstResponderKit.org', 0, 1, @CurrentDatabaseContext) WITH NOWAIT; RETURN; END @@ -284,6 +299,14 @@ Basic path sanity checks */ +IF @RunSetup = 1 and @BackupPath is NULL + BEGIN + + RAISERROR('@BackupPath is required during setup', 0, 1) WITH NOWAIT; + + RETURN; + END + IF (@BackupPath NOT LIKE '[c-zC-Z]:\%') --Local path, don't think anyone has A or B drives AND (@BackupPath NOT LIKE '\\[a-zA-Z0-9]%\%') --UNC path @@ -606,6 +629,8 @@ BEGIN INSERT msdb.dbo.restore_configuration (database_name, configuration_name, configuration_description, configuration_setting) VALUES ('all', 'log restore path', 'The path to which Log Restores come from.', @RestorePath); + INSERT msdb.dbo.restore_configuration (database_name, configuration_name, configuration_description, configuration_setting) + VALUES ('all', 'move files', 'Determines if we move database files to default data/log directories.', @MoveFiles); IF OBJECT_ID('msdb.dbo.restore_worker') IS NULL diff --git a/Deprecated/sp_BlitzCache_SQL_Server_2005.sql b/Deprecated/sp_BlitzCache_SQL_Server_2005.sql new file mode 100644 index 000000000..e31e5f332 --- /dev/null +++ b/Deprecated/sp_BlitzCache_SQL_Server_2005.sql @@ -0,0 +1,44 @@ +/* (C) Copyright 2014 Brent Ozar Unlimited, LLC */ + +/* +First off, the bad news: there is no sp_BlitzCache for SQL Server 2005. +SQL 2005 just doesn't have all the cool DMV and query features that we would +really need. However, there's still hope - this query will get you the top 20 +most resource-intensive queries in the plan cache. + +Note the ORDER BY - you can choose whether to order by CPU, duration (query +runtime), logical reads, or the number of times the query was executed. + +Happy tuning! +*/ + +SELECT TOP 20 total_worker_time / execution_count AS AvgCPU , + total_worker_time AS TotalCPU , + CAST(ROUND(100.00 * total_worker_time / (SELECT SUM(total_worker_time) FROM sys.dm_exec_query_stats), 2) AS MONEY) AS PercentCPU, + total_elapsed_time / execution_count AS AvgDuration , + total_elapsed_time AS TotalDuration , + CAST(ROUND(100.00 * total_elapsed_time / (SELECT SUM(total_elapsed_time) FROM sys.dm_exec_query_stats), 2) AS MONEY) AS PercentDuration, + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + CAST(ROUND(100.00 * total_logical_reads / (SELECT SUM(total_logical_reads) FROM sys.dm_exec_query_stats), 2) AS MONEY) AS PercentReads, + execution_count , + CAST(ROUND(100.00 * execution_count / (SELECT SUM(execution_count) FROM sys.dm_exec_query_stats), 2) AS MONEY) AS PercentExecutions, + executions_per_minute = CASE DATEDIFF(mi, creation_time, qs.last_execution_time) + WHEN 0 THEN 0 + ELSE CAST((1.00 * execution_count / DATEDIFF(mi, creation_time, qs.last_execution_time)) AS money) + END, + qs.creation_time AS plan_creation_time, + qs.last_execution_time, + SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , + query_plan + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp + ORDER BY TotalCPU DESC; + --ORDER BY TotalDuration DESC; + --ORDER BY TotalReads DESC; + --ORDER BY execution_count DESC; +GO diff --git a/Deprecated/sp_BlitzInMemoryOLTP.sql b/Deprecated/sp_BlitzInMemoryOLTP.sql new file mode 100644 index 000000000..fd622d9fb --- /dev/null +++ b/Deprecated/sp_BlitzInMemoryOLTP.sql @@ -0,0 +1,2053 @@ +DECLARE @msg NVARCHAR(MAX) = N''; + + -- Must be a compatible, on-prem version of SQL (2014+) +IF ( (SELECT SERVERPROPERTY ('EDITION')) <> 'SQL Azure' + AND (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) < 12 + ) + -- or Azure Database (not Azure Data Warehouse), running at database compat level 120+ +OR ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' + AND (SELECT SERVERPROPERTY ('ENGINEEDITION')) = 5 + AND (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 120 + ) +BEGIN + SELECT @msg = N'Sorry, sp_BlitzInMemoryOLTP doesn''t work on versions of SQL prior to 2014.' + REPLICATE(CHAR(13), 7933); + PRINT @msg; + RETURN; +END; + + +IF OBJECT_ID('dbo.sp_BlitzInMemoryOLTP', 'P') IS NULL +EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzInMemoryOLTP AS SELECT 1;'); +GO + +ALTER PROCEDURE dbo.sp_BlitzInMemoryOLTP( + @instanceLevelOnly BIT = 0 + , @dbName NVARCHAR(4000) = N'ALL' + , @tableName NVARCHAR(4000) = NULL + , @debug BIT = 0 + , @Version VARCHAR(30) = NULL OUTPUT + , @VersionDate DATETIME = NULL OUTPUT + , @VersionCheckMode BIT = 0 +) +/* +.SYNOPSIS + Get detailed information about In-Memory SQL Server objects + +.DESCRIPTION + Get detailed information about In-Memory SQL Server objects + Tested on SQL Server: 2014, 2016, 2017 + tested on Azure SQL Database + NOT tested on Azure Managed Instances + +.PARAMETER @instanceLevelOnly + Only check instance In-Memory related information + +.PARAMETER @dbName + Check database In-Memory objects for specified database + +.PARAMETER @tableName + Check database In-Memory objects for specified tablename + +.PARAMETER @debug + Only PRINT dynamic sql statements without executing it + +.EXAMPLE + EXEC sp_BlitzInMemoryOLTP; + -- Get all In-memory information + +.EXAMPLE + EXEC sp_BlitzInMemoryOLTP @dbName = N'ಠ ಠ'; + -- Get In-memory information for database with name ಠ ಠ + +.EXAMPLE + EXEC sp_BlitzInMemoryOLTP @instanceLevelOnly = 1; + -- Get only instance In-Memory information + +.EXAMPLE + EXEC sp_BlitzInMemoryOLTP @debug = 1; + -- PRINT dynamic sql statements without executing it + +.LICENSE MIT +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. + +.NOTE + Author: Ned Otter + Version: 2.0 + Original link: http://nedotter.com/archive/2017/10/in-memory-oltp-diagnostic-script/ + Release Link: https://github.com/ktaranov/sqlserver-kit/blob/master/Stored_Procedure/dbo.sp_BlitzInMemoryOLTP.sql + Main Contributors: Ned Otter, Konstantin Taranov, Aleksey Nagorskiy + +*/ +AS +DECLARE @ScriptVersion VARCHAR(30); +SELECT @ScriptVersion = '1.8', @VersionDate = '20240222'; + +IF(@VersionCheckMode = 1) +BEGIN + SET @Version = @ScriptVersion; + RETURN; +END; + +BEGIN TRY + + SET NOCOUNT ON; + SET STATISTICS XML OFF; + + DECLARE @RunningOnAzureSQLDB BIT = 0; + + DECLARE @crlf VARCHAR(10) = CHAR(10); + + DECLARE @Edition NVARCHAR(MAX) = CAST(SERVERPROPERTY('Edition') AS NVARCHAR(128)) + , @errorMessage NVARCHAR(512); + + DECLARE @MSSQLVersion INT = CONVERT(INT, SERVERPROPERTY('ProductMajorVersion')); + + IF @debug = 1 PRINT('--@MSSQLVersion = ' + CAST(@MSSQLVersion AS VARCHAR(30))); + + /* + ################################################### + if we get here, we are running at least SQL 2014, but that version + only runs In-Memory if we are using Enterprise Edition + + NOTE: Azure SQL database changes this equation + ################################################### + */ + + /* + SERVERPROPERTY('EngineEdition') + 1 = Personal or Desktop Engine (Not available in SQL Server 2005 and later versions.) + ,2 = Standard (This is returned for Standard, Web, and Business Intelligence.) + ,3 = Enterprise (This is returned for Evaluation, Developer, and both Enterprise editions.) + ,4 = Express (This is returned for Express, Express with Tools and Express with Advanced Services) + ,5 = SQL Database + ,6 = SQL Data Warehouse + ,8 = Managed Instance + */ + + SELECT @RunningOnAzureSQLDB = + CASE + WHEN SERVERPROPERTY('EngineEdition') IN (5, 6, 8) THEN 1 + ELSE 0 + END; + + -- Database level: we are running SQL Database or SQL Data Warehouse, but this specific database does not support XTP + IF (@RunningOnAzureSQLDB = 1 AND DatabasePropertyEx(DB_NAME(), 'IsXTPSupported') = 0) + BEGIN + SET @errorMessage = 'For Azure SQL Database, In-Memory OLTP is only suppported on the Premium tier'; + THROW 55001, @errorMessage, 1; + END; + + -- not on Azure, so we need to check versions/Editions + -- SQL 2014 only supports XTP on Enterprise edition + IF (SERVERPROPERTY('EngineEdition') IN (2, 4)) AND @MSSQLVersion = 12 AND (@Edition NOT LIKE 'Enterprise%' AND @Edition NOT LIKE 'Developer%') + BEGIN + SET @errorMessage = CONCAT('For SQL 2014, In-Memory OLTP is only suppported on Enterprise Edition. You are running SQL Server edition: ', @Edition); + THROW 55002, @errorMessage, 1; + END; + + -- We're not running on Azure, so we need to check versions/Editions + -- SQL 2016 non-Enterprise only supports XTP after SP1 + DECLARE @BuildString VARCHAR(4) = CONVERT(VARCHAR(4), SERVERPROPERTY('ProductBuild')); + + IF (SERVERPROPERTY('EngineEdition') IN (2, 4)) AND @MSSQLVersion = 13 AND (@BuildString < 4001) + -- 13.0.4001.0 is the minimum build for XTP support + BEGIN + SET @errorMessage = 'For SQL 2016, In-Memory OLTP is only suppported on non-Enterprise Edition as of SP1'; + THROW 55003, @errorMessage, 1; + END; + + /* + ###################################################################################################################### + DATABASE LEVEL + ###################################################################################################################### + */ + + DECLARE @resultsDatabaseLayout TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,fileGroupName NVARCHAR(MAX) + ,fileName NVARCHAR(MAX) + ,[containerName/fileType] NVARCHAR(MAX) + ,Name NVARCHAR(MAX) + ,IsContainer NVARCHAR(MAX) + ,fileGroupDescription NVARCHAR(MAX) + ,fileGroupState NVARCHAR(MAX) + ,sizeKB NVARCHAR(MAX) + ,sizeMB NVARCHAR(MAX) + ,sizeGB NVARCHAR(MAX) + ,totalSizeMB NVARCHAR(MAX) + ); + + DECLARE @resultsNativeModuleCount TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,[Number of modules] INT + ); + + DECLARE @resultsInMemTables TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,tableName NVARCHAR(MAX) + ,[rowCount] INT + ,durability_desc NVARCHAR(MAX) + ,temporal_type_desc NVARCHAR(MAX) + ,memoryAllocatedForTableKB NVARCHAR(MAX) + ,memoryUsedByTableKB NVARCHAR(MAX) + ,memoryAllocatedForIndexesKB NVARCHAR(MAX) + ,memoryUsedByIndexesKB NVARCHAR(MAX) + ); + + DECLARE @resultsIndexes TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,tableName NVARCHAR(MAX) + ,indexName NVARCHAR(MAX) + ,memory_consumer_id INT + ,consumerType NVARCHAR(MAX) + ,description NVARCHAR(MAX) + ,allocations INT + ,allocatedBytesMB NVARCHAR(MAX) + ,usedBytesMB NVARCHAR(MAX) + ); + + DECLARE @resultsHashBuckets TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,[Schema] NVARCHAR(MAX) + ,TableName NVARCHAR(MAX) + ,indexName NVARCHAR(MAX) + ,totalBucketCount BIGINT + ,emptyBucketCount BIGINT + ,emptyBucketPercent INT + ,avg_ChainLength INT + ,max_ChainLength BIGINT + ,[Free buckets status] NVARCHAR(MAX) + ,[avg_chain_length status] BIGINT + ); + + DECLARE @resultsIndexCount TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,tableName NVARCHAR(MAX) + ,indexCount INT + ); + + DECLARE @resultsNativeModules TABLE + ( + [object] NVARCHAR(MAX) + ,Name NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,[type] NVARCHAR(MAX) + ,[definition] NVARCHAR(MAX) + ); + + DECLARE @resultsNativeLoaded TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,moduleName NVARCHAR(MAX) + ,object_id INT + ); + + DECLARE @resultsTemporal TABLE + ( + [object] NVARCHAR(256) + ,databaseName NVARCHAR(MAX) + ,temporalTableSchema NVARCHAR(MAX) + ,temporalTableName NVARCHAR(MAX) + ,internalHistoryTableName NVARCHAR(MAX) + ,allocatedBytesForInternalHistoryTable BIGINT + ,usedBytesForInternalHistoryTable BIGINT + ); + + DECLARE @resultsMemoryConsumerForLOBs TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,tableName NVARCHAR(MAX) + ,columnName NVARCHAR(MAX) + ,typeDescription NVARCHAR(MAX) + ,memoryConsumerTypeDescription NVARCHAR(MAX) + ,memoryConsumerDescription NVARCHAR(MAX) + ,allocatedBytes INT + ,usedBytes INT + ); + + DECLARE @resultsTableTypes TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,[Schema] NVARCHAR(MAX) + ,[Name] NVARCHAR(MAX) + ); + + DECLARE @resultsNativeModuleStats TABLE + ( + [object] NVARCHAR(MAX) + ,databaseName NVARCHAR(MAX) + ,object_id INT + ,object_name NVARCHAR(MAX) + ,cached_time DATETIME + ,last_execution_time DATETIME + ,execution_count INT + ,total_worker_time INT + ,last_worker_time INT + ,min_worker_time INT + ,max_worker_time INT + ,total_elapsed_time INT + ,last_elapsed_time INT + ,min_elapsed_time INT + ,max_elapsed_time INT + + ); + + DECLARE @resultsxtp_storage_percent TABLE + ( + databaseName NVARCHAR(MAX) + ,end_time DATETIME + ,xtp_storage_percent DECIMAL(5, 2) + + ); + + CREATE TABLE #resultsContainerDetails + ( + [object] NVARCHAR(256) + ,databaseName NVARCHAR(256) + ,containerName NVARCHAR(256) + ,container_id BIGINT + ,sizeMB NVARCHAR(256) + ,fileCount INT + ); + + CREATE TABLE #resultsContainerFileDetails + ( + [object] NVARCHAR(256) + ,databaseName NVARCHAR(256) + ,containerName NVARCHAR(256) + ,container_id BIGINT + ,fileType NVARCHAR(256) + ,fileState NVARCHAR(256) + ,sizeBytes NVARCHAR(256) + ,sizeGB NVARCHAR(256) + ,fileCount INT + ,fileGroupState NVARCHAR(256) + ); + + CREATE TABLE #resultsContainerFileSummary + ( + [object] NVARCHAR(256) + ,databaseName NVARCHAR(256) + ,fileType NVARCHAR(256) + ,fileState NVARCHAR(256) + ,sizeBytes NVARCHAR(256) + ,sizeMB NVARCHAR(256) + ,fileCount INT + ,fileGroupState NVARCHAR(256) + ); + + IF OBJECT_ID('tempdb..#inmemDatabases') IS NOT NULL DROP TABLE #inmemDatabases; + + /* + -- IF we are searching for a specific @tablename, it could exist in >1 database. + -- This is the point at which we should filter, but it might require dynamic SQL, + -- or deleting database names that don't have an object where the name matches. + + */ + + SELECT QUOTENAME(name) AS name + , database_id + , ROW_NUMBER() OVER (ORDER BY name ASC) AS rowNumber + INTO #inmemDatabases + FROM sys.databases + WHERE name NOT IN ( 'master', 'model', 'tempdb', 'distribution', 'msdb', 'SSISDB') + AND 1 = + CASE + WHEN @RunningOnAzureSQLDB = 1 THEN 1 + WHEN @RunningOnAzureSQLDB = 0 AND name = @dbName THEN 1 + WHEN @RunningOnAzureSQLDB = 0 AND @dbName = N'ALL' THEN 1 + ELSE 0 + END + AND state_desc = 'ONLINE'; + + DECLARE @sql NVARCHAR(MAX) = '' + + DECLARE @counter INT = 1 + , @MaxRows INT = (SELECT COUNT(*) FROM #inmemDatabases); + + WHILE @counter <= @MaxRows + BEGIN + + --IF @debug = 1 PRINT('--@counter = ' + CAST(@counter AS VARCHAR(30)) + ';' + @crlf); + + IF @tableName IS NOT NULL + SELECT @sql = + CONCAT + ( + 'DELETE #inmemDatabases ' + ,'WHERE UPPER(name) = ' + ,'''' + ,UPPER(name) + ,'''' + ,' AND NOT EXISTS (' + ,'SELECT * + FROM ' + ,name + ,'.sys.objects + WHERE UPPER(name) = ' + ,'''' + ,UPPER(@tableName) + ,'''' + ,' AND UPPER(type) = ''U'')' + ) + FROM #inmemDatabases + WHERE rowNumber = @counter; + + IF @debug = 1 PRINT(@sql); + EXEC (@sql) + + SELECT @counter += 1; + END; + + ALTER TABLE #inmemDatabases + ADD newRowNumber INT IDENTITY + + IF @debug = 1 + SELECT 'All ONLINE user databases' AS AllDatabases + ,name + ,database_id + FROM #inmemDatabases; + + IF @dbName IS NULL AND @instanceLevelOnly = 0 + BEGIN + SET @errorMessage = '@dbName IS NULL, please specify database name or ALL'; + THROW 55004, @errorMessage, 1; + RETURN; + END; + + IF (@dbName IS NOT NULL AND @dbName <> N'ALL') + AND (NOT EXISTS (SELECT 1 FROM #inmemDatabases WHERE name = QUOTENAME(@dbName)) AND @instanceLevelOnly = 0) + BEGIN + SET @errorMessage = N'Database [' + @dbName + N'] not found in sys.databases!!!' + @crlf + + N'Did you add N if your database has a unicode name?' + @crlf + + N'Try to exec this: EXEC sp_BlitzInMemoryOLTP @dbName = N''ಠ ಠ_Your_Unicode_DB_Name_ಠ ಠ'''; + THROW 55005, @errorMessage, 1; + RETURN; + END; + + IF @dbName = 'ALL' AND NOT EXISTS (SELECT 1 FROM #inmemDatabases) + BEGIN + SET @errorMessage = 'ALL was specified, but no memory-optimized databases were found'; + THROW 55006, @errorMessage, 1; + RETURN; + END; + + -- we can't reference sys.dm_os_loaded_modules if we're on Azure SQL DB + IF @RunningOnAzureSQLDB = 0 + BEGIN + IF OBJECT_ID('tempdb..#moduleSplit') IS NOT NULL DROP TABLE #moduleSplit; + + CREATE TABLE #moduleSplit + ( + rowNumber INT IDENTITY PRIMARY KEY + ,value NVARCHAR(MAX) NULL + ); + + DECLARE @loadedModules TABLE + ( + rowNumber INT IDENTITY PRIMARY KEY + ,name NVARCHAR(MAX) NULL + ); + + INSERT @loadedModules + ( + name + ) + SELECT name + FROM sys.dm_os_loaded_modules AS a + WHERE description = 'XTP Native DLL' + AND PATINDEX('%[_]p[_]%', name) > 0; + + DECLARE @maxLoadedModules INT = (SELECT COUNT(*) FROM @loadedModules); + DECLARE @moduleCounter INT = 1; + DECLARE @loadedModuleName NVARCHAR(MAX) = ''; + + SET @moduleCounter = 1; + + WHILE @moduleCounter <= @maxLoadedModules + BEGIN + + SELECT @loadedModuleName = name + FROM @loadedModules + WHERE rowNumber = @moduleCounter; + + DECLARE @xml XML + , @delimiter NVARCHAR(10); + SET @delimiter = '_'; + SET @xml = CAST((''+REPLACE(@loadedModuleName, @delimiter, '')+'') AS XML); + + INSERT #moduleSplit + ( + value + ) + SELECT C.value('.', 'NVARCHAR(1000)') AS value FROM @xml.nodes('X') as X(C); + + SELECT @moduleCounter += 1; + + END; + END; + + IF @instanceLevelOnly = 0 + BEGIN + + /* + #################################################### + Determine which databases are memory-optimized + NOTE: if we are running on Azure SQL DB, we need + to verify in-memory capability without joining to + sys.filegroups + #################################################### + */ + SELECT @MaxRows = (SELECT COUNT(*) FROM #inmemDatabases); + SELECT @counter = 1 + + SELECT @sql = '' + + WHILE @counter <= @MaxRows + BEGIN + + --IF @debug = 1 PRINT('--@counter = ' + CAST(@counter AS VARCHAR(30)) + ';' + @crlf); + + IF @counter = 1 + BEGIN + SELECT @sql += ';WITH InMemDatabases AS ('; + END; + + SELECT @sql += + CASE + WHEN @counter = 1 THEN '' -- there is exactly 1 database for the entire instance + ELSE @crlf + ' UNION ALL ' + @crlf + END; + + SELECT @sql += + CASE WHEN @RunningOnAzureSQLDB = 0 THEN + CONCAT + ( + @crlf + ,'SELECT DISTINCT ' + , 'N''' + , name + , ''' AS databaseName,' + @crlf + , database_id + , ' AS database_id' + @crlf+ ' FROM ' + , name + , '.sys.database_files' + @crlf + ' INNER JOIN ' + , name + , '.sys.filegroups ON database_files.data_space_id = filegroups.data_space_id ' + , 'WHERE filegroups.type = ' + ,'''' + ,'FX' + ,'''' + ) + ELSE + -- if we arrive here and we're running on Azure SQL DB, then the database inherently supports In-Memory OLTP + CONCAT + ( + @crlf + ,'SELECT ' + , 'N''' + , name + , ''' AS databaseName,' + @crlf + , database_id + , ' AS database_id' + @crlf + ) + END + FROM #inmemDatabases + WHERE newRowNumber = @counter; + + SELECT @counter += 1; + END; + + --IF @debug = 1 PRINT(@sql); + + + -- post-processing + SELECT @sql += + CONCAT + ( + ')' + ,@crlf + ,'SELECT InMemDatabases.*, sys.databases.log_reuse_wait_desc' + ,@crlf + ,'FROM InMemDatabases ' + ,@crlf + ,'INNER JOIN sys.databases ON ' + ,'QUOTENAME(sys.databases.name) = InMemDatabases.databaseName;' + ); + + IF @debug = 1 + PRINT('--Determine which databases are memory-optimized' + @crlf + @sql + @crlf); + + DECLARE @RowCount INT = (SELECT COUNT(*) FROM #inmemDatabases); + + IF @RowCount <> 0 + BEGIN + + IF OBJECT_ID('tempdb..#MemoryOptimizedDatabases') IS NOT NULL DROP TABLE #MemoryOptimizedDatabases; + + CREATE TABLE #MemoryOptimizedDatabases + ( + rowNumber INT IDENTITY + , dbName NVARCHAR(256) NOT NULL + , database_id INT NULL + , log_reuse_wait_desc NVARCHAR(256) + ); + + INSERT #MemoryOptimizedDatabases + ( + dbName + ,database_id + ,log_reuse_wait_desc + ) + EXECUTE sp_executesql @sql; + + --IF @debug = 1 PRINT(@sql + @crlf); + --ELSE + --BEGIN + SELECT 'Memory-optimized database(s)' AS databases + ,dbName + ,database_id + ,log_reuse_wait_desc + FROM #MemoryOptimizedDatabases + ORDER BY dbName; + --END; + END; + + IF OBJECT_ID('tempdb..#NativeModules') IS NOT NULL DROP TABLE #NativeModules; + + CREATE TABLE #NativeModules + ( + moduleKey INT IDENTITY NOT NULL + ,moduleID INT NOT NULL + ,moduleName NVARCHAR(256) NOT NULL + ,collectionStatus BIT NULL + ); + + SELECT @sql = ''; + DECLARE @dbCounter INT = 1; + SELECT @MaxRows = COUNT(*) FROM #MemoryOptimizedDatabases; + DECLARE @databaseID INT = 1; + + + /* + ################################################### + This is the loop that processes each db + ################################################### + */ + + WHILE @dbCounter <= @MaxRows + BEGIN + + SELECT 'now processing database: ' + dbName AS Status + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + /* + ################################################### + List memory-optimized tables in this database + ################################################### + */ + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''Memory optimized tables''' + , ' AS [object],' + , ' N''' + ,dbName + ,''' AS databaseName' + ,', b.name AS tableName + , p.rows AS [rowCount] + ,durability_desc ' + ,CASE WHEN @MSSQLVersion >= 13 THEN ', temporal_type_desc ' ELSE ',NULL AS temporal_type_desc' END + ,', FORMAT(memory_allocated_for_table_kb, ''###,###,###'') AS memoryAllocatedForTableKB + ,FORMAT(memory_used_by_table_kb, ''###,###,###'') AS memoryUsedByTableKB + ,FORMAT(memory_allocated_for_indexes_kb, ''###,###,###'') AS memoryAllocatedForIndexesKB + ,FORMAT(memory_used_by_indexes_kb, ''###,###,###'') AS memoryUsedByIndexesKB + FROM ' + , dbName + ,'.sys.dm_db_xtp_table_memory_stats a' + ,' INNER JOIN ' + , dbName + ,'.sys.tables b ON b.object_id = a.object_id' + ,' INNER JOIN ' + ,dbName + ,'.sys.partitions p' + ,' ON p.[object_id] = b.[object_id]' + ,' INNER JOIN ' + ,dbName + ,'.sys.schemas s' + ,' ON b.[schema_id] = s.[schema_id]' + ,' WHERE p.index_id = 2' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' AND b.name = ', '''', @tableName, ''''); + END; + + IF @debug = 1 + PRINT('--List memory-optimized tables in this database' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsInMemTables + INSERT @resultsInMemTables + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsInMemTables) + SELECT * FROM @resultsInMemTables; + + END; + + /* + ############################################################## + List indexes on memory-optimized tables in this database + ############################################################## + */ + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''List indexes on memory-optimized tables in this database'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,t.name AS tableName + ,i.name AS indexName + ,c.memory_consumer_id + ,c.memory_consumer_type_desc AS consumerType + ,c.memory_consumer_desc AS description + ,c.allocation_count AS allocations + ,FORMAT(c.allocated_bytes / 1024.0, ''###,###,###,###'') AS allocatedBytesMB + ,FORMAT(c.used_bytes / 1024.00, ''###,###,###,###.###'') AS usedBytesMB + FROM ' + ,dbName + ,'.sys.dm_db_xtp_memory_consumers c + INNER JOIN ' + ,dbName + ,'.sys.tables t ON t.object_id = c.object_id' + ,CASE WHEN @MSSQLVersion > 12 THEN ' INNER JOIN sys.memory_optimized_tables_internal_attributes a ON a.object_id = c.object_id + AND a.xtp_object_id = c.xtp_object_id' ELSE NULL END + ,@crlf + ' LEFT JOIN ' + ,dbName + ,'.sys.indexes i ON c.object_id = i.object_id + AND c.index_id = i.index_id ' + ,CASE WHEN @MSSQLVersion > 12 THEN 'AND a.minor_id = 0' ELSE NULL END + ,@crlf + ' WHERE t.type = ' + , '''u''' + , ' AND t.is_memory_optimized = 1 ' + ,' AND i.index_id IS NOT NULL' + + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' AND t.name = ', '''', @tableName, ''''); + END; + + SELECT @sql += ' ORDER BY tableName, indexName;' + + IF @debug = 1 + PRINT('--List indexes on memory-optimized tables in this database' + @crlf + @sql + @crlf); + ELSE + BEGIN + DELETE @resultsIndexes + INSERT @resultsIndexes + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsIndexes) + SELECT * FROM @resultsIndexes; + + END; + + /* + ######################################################### + verify avg_chain_length for HASH indexes + + From BOL: + + Empty buckets: + 33% is a good target value, but a larger percentage (even 90%) is usually fine. + When the bucket count equals the number of distinct key values, approximately 33% of the buckets are empty. + A value below 10% is too low. + + Chains within buckets: + An average chain length of 1 is ideal in case there are no duplicate index key values. Chain lengths up to 10 are usually acceptable. + If the average chain length is greater than 10, and the empty bucket percent is greater than 10%, + the data has so many duplicates that a hash index might not be the most appropriate type. + + ######################################################### + */ + + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''avg_chain_length for HASH indexes''' + ,' AS [object],''' + ,dbName + ,'''' + ,' AS databaseName' + ,', sch.name AS [Schema] ' + ,', t.name AS tableName + ,i.name AS [indexName] + ,h.total_bucket_count AS totalBucketCount + ,h.empty_bucket_count AS emptyBucketCount + ,FLOOR((CAST(h.empty_bucket_count AS FLOAT) / h.total_bucket_count) * 100) AS [emptybBucketPercent] + ,h.avg_chain_length AS avg_ChainLength + ,h.max_chain_length AS maxChainLength + ,IIF(FLOOR((CAST(h.empty_bucket_count AS FLOAT) / h.total_bucket_count) * 100) < 33, ''Free buckets % is low!'', '''') AS [Free buckets status] + ,IIF(h.avg_chain_length > 10 AND FLOOR((CAST(h.empty_bucket_count AS FLOAT) / h.total_bucket_count) * 100) > 10, ''avg_chain_length has many collisions!'', '''') AS [avg_chain_length status] + FROM ' + ,dbName + ,'.sys.dm_db_xtp_hash_index_stats AS h + INNER JOIN ' + ,dbName + ,'.sys.indexes AS i ON h.object_id = i.object_id AND h.index_id = i.index_id' + ,CASE WHEN @MSSQLVersion > 12 THEN + CONCAT(' INNER JOIN ', dbName ,'.sys.memory_optimized_tables_internal_attributes ia ON h.xtp_object_id = ia.xtp_object_id') ELSE NULL END + ,' INNER JOIN ' + ,dbName + ,'.sys.tables t ON h.object_id = t.object_id' + ,' INNER JOIN ' + ,dbName + ,'.sys.schemas sch ON sch.schema_id = t.schema_id ' + ,CASE WHEN @MSSQLVersion > 12 THEN 'WHERE ia.type = 1' ELSE NULL END + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' AND t.name = ', '''', @tableName, ''''); + END; + + SELECT @sql += ' ORDER BY sch.name + ,t.name + ,i.name;'; + + IF @debug = 1 + PRINT('--Verify avg_chain_length for HASH indexes' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsHashBuckets + INSERT @resultsHashBuckets + EXECUTE sp_executesql @sql; +; + IF EXISTS(SELECT 1 FROM @resultsHashBuckets) + SELECT * FROM @resultsHashBuckets; + + END; + + + /* + ######################################################### + Count of indexes per table in this database + ######################################################### + */ + + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''Number of indexes per table'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,t.name AS tableName + ,COUNT(DISTINCT i.index_id) AS indexCount + FROM ' + ,dbName + ,'.sys.dm_db_xtp_memory_consumers c + INNER JOIN ' + ,dbName + ,'.sys.tables t ON t.object_id = c.object_id' + ,CASE WHEN @MSSQLVersion > 12 THEN + CONCAT(' INNER JOIN ', dbName ,'.sys.memory_optimized_tables_internal_attributes a ON a.object_id = c.object_id + AND a.xtp_object_id = c.xtp_object_id') ELSE NULL END + ,' LEFT JOIN ' + ,dbName + ,'.sys.indexes i ON c.object_id = i.object_id + AND c.index_id = i.index_id ' + ,CASE WHEN @MSSQLVersion > 12 THEN ' AND a.minor_id = 0' ELSE NULL END + ,' WHERE t.type = ' + , '''u''' + , ' AND t.is_memory_optimized = 1 ' + ,' AND i.index_id IS NOT NULL' + --,' GROUP BY t.name + -- ORDER BY t.name' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' AND t.name = ', '''', @tableName, ''''); + END; + + SELECT @sql += + ' GROUP BY t.name + ORDER BY t.name'; + + IF @debug = 1 + PRINT('--Count of indexes per table in this database' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsIndexCount + INSERT @resultsIndexCount + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsIndexCount) + SELECT * FROM @resultsIndexCount; + + END; + + + /* + ##################################################### + List natively compiled modules in this database + ##################################################### + */ + /* + + FN = SQL scalar function + IF = SQL inline table-valued function + TF = SQL table-valued-function + TR = SQL DML trigger + */ + + IF @tableName IS NULL + BEGIN + + SELECT @sql = + CONCAT + ( + 'SELECT ''Natively compiled modules'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,A.name + ,CASE A.type + WHEN ''FN'' THEN ''Function'' + WHEN ''P'' THEN ''Procedure'' + WHEN ''TR'' THEN ''Trigger'' + END AS type + ,B.definition AS [definition] + FROM ' + , dbName + ,'.sys.all_objects AS A + INNER JOIN ' + ,dbName + ,'.sys.sql_modules AS B ON B.object_id = A.object_id + WHERE UPPER(B.definition) LIKE ''%NATIVE_COMPILATION%'' + AND UPPER(A.name) <> ''SP_BLITZINMEMORYOLTP'' + ORDER BY A.type, A.name' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--List natively compiled modules in this database' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsNativeModules + INSERT @resultsNativeModules + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsNativeModules) + SELECT * FROM @resultsNativeModules; + + END; + END; + + /* + ##################################################### + List *loaded* natively compiled modules in this database, i.e. executed at least 1x + ##################################################### + */ + + /* + the format for checkpoint files changed from SQL 2014 to SQL 2016 + + SQL 2014 format: + database_id = 5 + object_id = 309576141 + + H:\SQLDATA\xtp\5\xtp_p_5_309576141.dll + + SQL 2016+ format + database_id = 9 + object_id = 1600880920 + + H:\SQLDATA\xtp\9\xtp_p_9_1600880920_185048689287400_1.dll + + the following code should handle all versions + */ + + -- NOTE: disabling this for Azure SQL DB + IF @tableName IS NULL AND @RunningOnAzureSQLDB = 0 + BEGIN + SELECT @sql = + CONCAT + ( + ';WITH nativeModuleObjectID AS + ( + SELECT DISTINCT REPLACE(value, ''.dll'', '''') AS object_id + FROM #moduleSplit + WHERE rowNumber % ' + ,CASE WHEN @MSSQLVersion = 12 THEN ' 4 = 0' + ELSE ' 6 = 4' -- @MSSQLVersion >= 13 + END + ,')' + ); + + SELECT @sql += + CONCAT + ( + 'SELECT ''Loaded natively compiled modules'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,name AS moduleName + ,procedures.object_id + FROM ' + ,dbName + ,'.sys.all_sql_modules + INNER JOIN ' + ,dbName + ,'.sys.procedures ON procedures.object_id = all_sql_modules.object_id + INNER JOIN nativeModuleObjectID ON nativeModuleObjectID.object_id = procedures.object_id' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--List loaded natively compiled modules in this database (@MSSQLVersion >= 13)' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsNativeLoaded + INSERT @resultsNativeLoaded + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsNativeLoaded) + SELECT * FROM @resultsNativeLoaded; + + END; + END; + + /* + ######################################################### + Count of natively compiled modules in this database + ######################################################### + */ + IF @tableName IS NULL + BEGIN + SELECT @sql = + CONCAT + ( + 'SELECT ''Count of natively compiled modules'' AS [object],' + ,' N''' + ,dbName + ,' ''' + ,' AS databaseName + , COUNT(*) AS [Number of modules] + FROM ' + , dbName + ,'.sys.all_sql_modules + INNER JOIN ' + ,dbName + ,'.sys.procedures ON procedures.object_id = all_sql_modules.object_id + WHERE uses_native_compilation = 1 + ORDER BY 1' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--Count of natively compiled modules in this database' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsNativeModuleCount + INSERT @resultsNativeModuleCount + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsNativeModuleCount WHERE [Number of modules] > 0) + SELECT * FROM @resultsNativeModuleCount; + + END; + END; + + /* + ############################################################ + Display memory consumption for temporal/internal tables + ############################################################ + */ + + -- temporal is supported in SQL 2016+ + IF @MSSQLVersion >= 13 + BEGIN + + SELECT @sql = + CONCAT + ( + ';WITH InMemoryTemporalTables + AS + ( + SELECT ' + ,'''' + ,'In-Memory Temporal Tables' + ,'''' + ,'AS object,' + ,'N''' + ,dbName + ,'''' + ,' AS databaseName' + ,',sch.name AS temporalTableSchema + ,T1.OBJECT_ID AS temporalTableObjectId + ,IT.OBJECT_ID AS internalTableObjectId + ,T1.name AS temporalTableName + ,IT.Name AS internalHistoryTableName + FROM ' + ,dbName + ,'.sys.internal_tables IT + INNER JOIN ' + ,dbName + ,'.sys.tables T1 ON IT.parent_OBJECT_ID = T1.OBJECT_ID + INNER JOIN ' + ,dbName + ,'.sys.schemas sch ON sch.schema_id = T1.schema_id + WHERE T1.is_memory_optimized = 1 + AND T1.temporal_type = 2 + ) + ,DetailedConsumption + AS + ( + SELECT object + ,databaseName + ,temporalTableSchema + ,T.temporalTableName + ,T.internalHistoryTableName + ,CASE + WHEN C.object_id = T.temporalTableObjectId + THEN ''Temporal Table'' + ELSE ''Internal Table'' + END AS ConsumedBy + ,C.allocated_bytes + ,C.used_bytes + FROM ' + ,dbName + ,'.sys.dm_db_xtp_memory_consumers C + INNER JOIN InMemoryTemporalTables T + ON C.object_id = T.temporalTableObjectId OR C.object_id = T.internalTableObjectId + WHERE C.allocated_bytes > 0 + AND C.object_id <> T.temporalTableObjectId + ) + SELECT DISTINCT object + ,databaseName + ,temporalTableSchema + ,temporalTableName + ,internalHistoryTableName + ,SUM(allocated_bytes) OVER (PARTITION BY temporalTableName ORDER BY temporalTableName) AS allocatedBytesForInternalHistoryTable + ,SUM(used_bytes) OVER (PARTITION BY temporalTableName ORDER BY temporalTableName) AS usedBytesForInternalHistoryTable + FROM DetailedConsumption' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' WHERE temporalTableName = ', '''', @tableName, ''''); + END; + + IF @debug = 1 + PRINT('--Display memory consumption for temporal/internal tables' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsTemporal + INSERT @resultsTemporal + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsTemporal) + SELECT * FROM @resultsTemporal; + + END; + END; -- display memory consumption for temporal/internal tables + + /* + ######################################################### + Display memory structures for LOB columns (off-row) + for SQL 2016+ + ######################################################### + */ + + IF @MSSQLVersion >= 13 + BEGIN + + SELECT @sql = + CONCAT + ( + 'SELECT DISTINCT ' + ,'''LOB/Off-row data '' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName' + ,', OBJECT_NAME(a.object_id) AS tableName + ,cols.name AS columnName + ,a.type_desc AS typeDescription + ,c.memory_consumer_type_desc AS memoryConsumerTypeDescription + ,c.memory_consumer_desc AS memoryConsumerDescription + ,c.allocated_bytes AS allocatedBytes + ,c.used_bytes AS usedBytes + FROM ' + ,dbName + ,'.sys.dm_db_xtp_memory_consumers c + INNER JOIN ' + ,dbName + ,'.sys.memory_optimized_tables_internal_attributes a ON a.object_id = c.object_id + AND a.xtp_object_id = c.xtp_object_id ' + ,' INNER JOIN ' + ,dbName + ,'.sys.objects AS b ON b.object_id = a.object_id ' + ,' INNER JOIN ' + ,dbName + ,'.sys.syscolumns AS cols ON cols.id = b.object_id + WHERE a.type_desc = ' + ,'''' + ,'INTERNAL OFF-ROW DATA TABLE' + ,'''' + ,' AND c.memory_consumer_desc = ''Table heap''' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @tableName IS NOT NULL + BEGIN + SELECT @sql += CONCAT(' AND OBJECT_NAME(a.object_id) = ', '''', @tableName, ''''); + END; + + SELECT @sql += ' ORDER BY databaseName, tableName, columnName'; + + IF @debug = 1 + PRINT('--Display memory structures for LOB columns (off-row)' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsMemoryConsumerForLOBs + INSERT @resultsMemoryConsumerForLOBs + + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsMemoryConsumerForLOBs) + SELECT * FROM @resultsMemoryConsumerForLOBs; + + END; + + END; + + /* + ####################################################### + Display memory-optimized table types + ####################################################### + */ + IF @tableName IS NULL + BEGIN + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''Memory optimized table types'' AS [object],' + ,' N''' + ,dbName + ,''' AS databaseName,' + ,'SCHEMA_NAME(tt.schema_id) AS [Schema] + ,tt.name AS [Name] + FROM ' + ,dbName + ,'.sys.table_types AS tt + INNER JOIN ' + ,dbName + ,'.sys.schemas AS stt ON stt.schema_id = tt.schema_id + WHERE tt.is_memory_optimized = 1 + ORDER BY [Schema], tt.name ' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--Display memory-optimized table types' + @crlf + @sql + @crlf); + ELSE + BEGIN + + DELETE @resultsTableTypes + INSERT @resultsTableTypes + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsTableTypes) + SELECT * FROM @resultsTableTypes; + + END; + + END; + + /* + ################################################################## + ALL database files, including container name, size, location + ################################################################## + */ + + IF @tableName IS NULL + BEGIN + SELECT @sql = + CONCAT + ( + 'SELECT ' + ,'''Database layout'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName' + ,',filegroups.name AS fileGroupName + ,physical_name AS fileName + ,database_files.name AS [Name] + ,filegroups.type AS fileGroupType + ,IsContainer = IIF(filegroups.type = ''FX'', ''Yes'', ''No'') + ,filegroups.type_desc AS fileGroupDescription + ,database_files.state_desc AS fileGroupState + ,FORMAT(database_files.size * CONVERT(BIGINT, 8192) / 1024, ''###,###,###,###'') AS sizeKB + ,FORMAT(database_files.size * CONVERT(BIGINT, 8192) / 1048576.0, ''###,###,###,###'') AS sizeMB + ,FORMAT(database_files.size * CONVERT(BIGINT, 8192) / 1073741824.0, ''###,###,###,###.##'') AS sizeGB + ,FORMAT(SUM(database_files.size / 128.0) OVER(), ''###,###,###,###'') AS totalSizeMB + FROM ' + ,dbName + ,'.sys.database_files + LEFT JOIN ' + ,dbName + ,'.sys.filegroups ON database_files.data_space_id = filegroups.data_space_id + ORDER BY filegroups.type, filegroups.name, database_files.name' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--ALL database files, including container name, size, location' + @crlf + @sql + @crlf); + ELSE + BEGIN + DELETE @resultsDatabaseLayout + + INSERT @resultsDatabaseLayout + EXECUTE sp_executesql @sql; + + IF EXISTS(SELECT 1 FROM @resultsDatabaseLayout) + SELECT * FROM @resultsDatabaseLayout; + + END; + + /* + ################################################################## + container name, size, number of files + ################################################################## + */ + + DELETE #resultsContainerDetails; + + SELECT @sql = + CONCAT + ( + ';WITH ContainerDetails AS + ( + SELECT ' + ,' container_id + ,SUM(ISNULL(file_size_in_bytes, 0)) AS sizeinBytes + ,COUNT(*) AS fileCount + ,MAX(container_guid) AS container_guid + FROM ' + ,dbName + ,'.sys.dm_db_xtp_checkpoint_files + GROUP BY container_id + ) + INSERT #resultsContainerDetails + SELECT + ''Container details by container name'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,database_files.name AS containerName + ,ContainerDetails.container_id + ,FORMAT(ContainerDetails.sizeinBytes / 1048576., ''###,###,###'') AS sizeMB + ,ContainerDetails.fileCount + FROM ContainerDetails + INNER JOIN ' + ,dbName + ,'.sys.database_files ON ContainerDetails.container_guid = database_files.file_guid' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--container name, size, number of files' + @crlf + @sql + @crlf); + ELSE + BEGIN + EXECUTE sp_executesql @sql; + IF EXISTS (SELECT 1 FROM #resultsContainerDetails) + SELECT * FROM #resultsContainerDetails + END + + /* + ################################################################## + container file summary + ################################################################## + */ + + DELETE #resultsContainerFileSummary; + + SELECT @sql = + CONCAT + ( + ';WITH ContainerFileSummary AS + ( + SELECT ' + ,' + SUM(ISNULL(file_size_in_bytes, 0)) AS sizeinBytes + ,MAX(ISNULL(file_type_desc, '''')) AS fileType + ,COUNT(*) AS fileCount + ,MAX(state_desc) AS fileState + ,MAX(container_guid) AS container_guid + FROM ' + ,dbName + ,'.sys.dm_db_xtp_checkpoint_files + GROUP BY file_type_desc, state_desc + ) + INSERT #resultsContainerFileSummary + SELECT + ''Container details by fileType and fileState'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,ContainerFileSummary.fileType + ,ContainerFileSummary.fileState + ,FORMAT(ContainerFileSummary.sizeinBytes, ''###,###,###'') AS sizeBytes + ,FORMAT(ContainerFileSummary.sizeinBytes / 1048576., ''###,###,###'') AS sizeMB + ,ContainerFileSummary.fileCount + ,database_files.state_desc AS fileGroupState + FROM ContainerFileSummary + INNER JOIN ' + ,dbName + ,'.sys.database_files ON ContainerFileSummary.container_guid = database_files.file_guid' + ,' ORDER BY ContainerFileSummary.fileType, ContainerFileSummary.fileState;' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--container file summary' + @crlf + @sql + @crlf); + ELSE + BEGIN + EXECUTE sp_executesql @sql; + IF EXISTS (SELECT 1 FROM #resultsContainerFileSummary) + SELECT * FROM #resultsContainerFileSummary; + END; + + /* + ################################################################## + container file details + ################################################################## + */ + + DELETE #resultsContainerFileDetails; + + SELECT @sql = + CONCAT + ( + ';WITH ContainerFileDetails AS + ( + SELECT + container_id + ,SUM(ISNULL(file_size_in_bytes, 0)) AS sizeinBytes + ,MAX(ISNULL(file_type_desc, '''')) AS fileType + ,COUNT(*) AS fileCount + ,MAX(state_desc) AS fileState + ,MAX(container_guid) AS container_guid + FROM ' + ,dbName + ,'.sys.dm_db_xtp_checkpoint_files + GROUP BY container_id, file_type_desc, state_desc + ) + INSERT #resultsContainerFileDetails + SELECT ' + ,'''Container file details by container_id, fileType and fileState'' AS [object],' + ,' N''' + ,dbName + ,'''' + ,' AS databaseName + ,database_files.name AS containerName + ,ContainerFileDetails.container_id + ,ContainerFileDetails.fileType + ,ContainerFileDetails.fileState + ,FORMAT(ContainerFileDetails.sizeinBytes, ''###,###,###'') AS sizeBytes + ,FORMAT(ContainerFileDetails.sizeinBytes / 1048576., ''###,###,###'') AS sizeGB + ,ContainerFileDetails.fileCount + ,database_files.state_desc AS fileGroupState + FROM ContainerFileDetails + INNER JOIN ' + ,dbName + ,'.sys.database_files ON ContainerFileDetails.container_guid = database_files.file_guid' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + IF @debug = 1 + PRINT('--container details' + @crlf + @sql + @crlf); + ELSE + BEGIN + EXECUTE sp_executesql @sql; + IF EXISTS (SELECT 1 FROM #resultsContainerFileDetails) + SELECT * FROM #resultsContainerFileDetails; + END; + END; + /* + + ########################################################### + Report on whether or not execution statistics + for natively compiled procedures is enabled + ########################################################### + */ + + IF EXISTS (SELECT 1 FROM #NativeModules) + BEGIN + SELECT @sql = + CONCAT + ( + 'INSERT #NativeModules + ( + moduleID + ,moduleName + ) + SELECT ' + ,dbName + ,'.sys.all_sql_modules.Object_ID AS ObjectID + ,name AS moduleName + FROM ' + ,dbName + ,'.sys.all_sql_modules + INNER JOIN ' + ,dbName + ,'.sys.procedures ON procedures.object_id = all_sql_modules.object_id' + ,' WHERE uses_native_compilation = 1' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + EXECUTE sp_executesql @sql; + + DELETE @resultsNativeModuleStats; + + SELECT @sql = + CONCAT + ( + 'SELECT ''Native modules that have exec stats enabled'' AS object' + ,',' + ,' N''' + ,dbName + ,''' AS databaseName + ,object_id + ,OBJECT_NAME(object_id) AS ''object name'' + ,cached_time + ,last_execution_time + ,execution_count + ,total_worker_time + ,last_worker_time + ,min_worker_time + ,max_worker_time + ,total_elapsed_time + ,last_elapsed_time + ,min_elapsed_time + ,max_elapsed_time + FROM ' + ,'sys.dm_exec_procedure_stats + WHERE database_id = DB_ID() + AND object_id IN (SELECT object_id FROM sys.sql_modules WHERE uses_native_compilation = 1) + ORDER BY total_worker_time DESC;' + ) + FROM #MemoryOptimizedDatabases + WHERE rowNumber = @dbCounter; + + INSERT @resultsNativeModuleStats + EXECUTE sp_executesql @sql; + + IF @debug = 1 + PRINT('--Native modules with execution status' + @crlf + @sql + @crlf); + ELSE + IF EXISTS(SELECT 1 FROM @resultsNativeModuleStats) + SELECT * FROM @resultsNativeModuleStats; + + END; --IF EXISTS (SELECT 1 FROM #NativeModules) + + ELSE + BEGIN + PRINT '--No modules found that have collection stats enabled'; + END; + + IF @RunningOnAzureSQLDB = 1 + BEGIN + + DELETE @resultsxtp_storage_percent; + + INSERT @resultsxtp_storage_percent + ( + databaseName + ,end_time + ,xtp_storage_percent + ) + SELECT DB_NAME() AS databaseName + ,end_time + ,xtp_storage_percent + FROM sys.dm_db_resource_stats + WHERE xtp_storage_percent > 0; + + IF EXISTS(SELECT 1 FROM @resultsxtp_storage_percent) + BEGIN + SELECT databaseName + ,'xtp_storage_percent in descending order' AS object + ,end_time + ,xtp_storage_percent + FROM @resultsxtp_storage_percent + ORDER BY end_time DESC; + END; + + SELECT DB_NAME() AS databaseName + ,DBScopedConfig = 'XTP_PROCEDURE_EXECUTION_STATISTICS enabled:' + ,Status = CASE WHEN value = 1 THEN 'Yes' ELSE 'No' END + FROM sys.database_scoped_configurations + WHERE UPPER(name) = 'XTP_PROCEDURE_EXECUTION_STATISTICS'; + + SELECT DB_NAME() AS databaseName + ,DBScopedConfig = 'XTP_QUERY_EXECUTION_STATISTICS enabled:' + ,Status = CASE WHEN value = 1 THEN 'Yes' ELSE 'No' END + FROM sys.database_scoped_configurations + WHERE UPPER(name) = 'XTP_QUERY_EXECUTION_STATISTICS'; + END; + + SELECT @dbCounter += 1; + + END; -- This is the loop that processes each database + + END; -- IF @instanceLevelOnly = 0 + + + IF OBJECT_ID('#NativeModules', 'U') IS NOT NULL DROP TABLE #NativeModules; + + /* + ###################################################################################################################### + INSTANCE LEVEL + ###################################################################################################################### + */ + + + /* + ################################################### + Because SQL 2016/SP1 brings In-Memory OLTP to + editions other Enterprise, we must check + @@version + ################################################### + */ + + IF @instanceLevelOnly = 1 AND @MSSQLVersion >= 12 + BEGIN + + SELECT @@version AS Version; + + SELECT name + ,value AS configValue + ,value_in_use AS runValue + FROM sys.configurations + WHERE UPPER(name) LIKE 'MAX SERVER MEMORY%' + ORDER BY name OPTION (RECOMPILE); + + -- from Mark Wilkinson + /* + If memory is being used it should be in here. + + Memory that is reported as being consumed here for XTP was missing in + the other XTP DMVs. We should simply look to see what the highest consumer is. + + SELECT * FROM sys.dm_xtp_system_memory_consumers + + SELECT * FROM sys.dm_db_xtp_memory_consumers + + */ + + SELECT 'dm_os_memory_clerks, DETAILS' AS Object + ,type + ,name + ,pages_kb + ,virtual_memory_reserved_kb + ,virtual_memory_committed_kb + ,awe_allocated_kb + ,shared_memory_reserved_kb + ,shared_memory_committed_kb + FROM sys.dm_os_memory_clerks; + + SELECT 'dm_os_memory_clerks, SUMMARY by XTP type' AS Object + ,type AS object_type + ,SUM(pages_kb) /1024.0 /1024.0 AS pages_mb + FROM sys.dm_os_memory_clerks + WHERE type LIKE '%XTP%' + GROUP BY type; + + DECLARE @xtp_system_memory_consumers TABLE + ( + object_type nvarchar(64) + ,pagesAllocatedMB BIGINT + ,pagesUsedMB BIGINT + ); + + INSERT @xtp_system_memory_consumers + ( + object_type + ,pagesAllocatedMB + ,pagesUsedMB + ) + SELECT memory_consumer_type_desc AS object_type, + SUM(allocated_bytes) /1024.0 /1024.0 AS pagesAllocatedMB + ,SUM(allocated_bytes) /1024.0 /1024.0 AS pagesUsedMB + FROM sys.dm_xtp_system_memory_consumers + GROUP BY memory_consumer_type_desc + ORDER BY memory_consumer_type_desc; + + IF EXISTS (SELECT 1 FROM @xtp_system_memory_consumers) + SELECT 'xtp_system_memory_consumers' AS Object + ,object_type + ,pagesAllocatedMB + ,pagesUsedMB + FROM @xtp_system_memory_consumers; + + -- sys.dm_os_sys_info not supported on Azure SQL Database + IF @RunningOnAzureSQLDB = 0 + BEGIN + SELECT 'Committed Target memory' AS Object + ,FORMAT(committed_target_kb, '###,###,###,###,###') AS committedTargetKB + ,FORMAT(committed_target_kb / 1024, '###,###,###,###,###') AS committedTargetMB + ,FORMAT(committed_target_kb / 1048576, '###,###,###,###,###') AS committedTargetGB + FROM sys.dm_os_sys_info; + END + + IF OBJECT_ID('#TraceFlags', 'U') IS NOT NULL DROP TABLE #TraceFlags; + + CREATE TABLE #TraceFlags + ( + TraceFlag INT NOT NULL + ,Status TINYINT NOT NULL + ,Global TINYINT NOT NULL + ,Session TINYINT NOT NULL + ); + + SET @sql = 'DBCC TRACESTATUS'; + + INSERT #TraceFlags + + EXECUTE sp_executesql @sql; + + IF @debug = 1 + PRINT(@crlf + @sql + @crlf); + + DECLARE @msg NVARCHAR(MAX); + + IF EXISTS (SELECT 1 FROM #TraceFlags WHERE TraceFlag = 10316) -- allows custom indexing on hidden staging table for temporal tables + BEGIN + + SELECT @msg = 'TraceFlag 10316 is enabled'; + + SELECT @msg + ,TraceFlag + ,Status + ,Global + ,Session + FROM #TraceFlags + WHERE TraceFlag = 10316 + ORDER BY TraceFlag; + + END; + + /* + ############################################################################################# + Verify if collection statistics are enabled for: + 1. specific native modules + 2. all native modules (instance-wide config) + + Having collection statistics enabled can severely impact performance of native modules. + ############################################################################################# + */ + + -- instance level + DECLARE @InstancecollectionStatus BIT; + + IF @RunningOnAzureSQLDB = 0 + BEGIN + + EXEC sys.sp_xtp_control_query_exec_stats + @old_collection_value = @InstancecollectionStatus OUTPUT; + + SELECT + CASE + WHEN @InstancecollectionStatus = 1 THEN 'YES' + ELSE 'NO' + END AS [instance-level collection of execution statistics for Native Modules enabled]; + END; + ELSE + BEGIN + -- repeating this from the database section if we are running @instanceLevelOnly = 1 + + DELETE @resultsxtp_storage_percent; + + INSERT @resultsxtp_storage_percent + ( + databaseName + ,end_time + ,xtp_storage_percent + ) + SELECT DB_NAME() AS databaseName + ,end_time + ,xtp_storage_percent + FROM sys.dm_db_resource_stats + WHERE xtp_storage_percent > 0; + + IF EXISTS(SELECT 1 FROM @resultsxtp_storage_percent) + BEGIN + SELECT databaseName + ,'xtp_storage_percent in descending order' AS object + ,end_time + ,xtp_storage_percent + FROM @resultsxtp_storage_percent + ORDER BY end_time DESC; + END; + + SELECT DB_NAME() AS databaseName + ,DBScopedConfig = 'XTP_PROCEDURE_EXECUTION_STATISTICS enabled:' + ,Status = CASE WHEN value = 1 THEN 'Yes' ELSE 'No' END + FROM sys.database_scoped_configurations + WHERE UPPER(name) = 'XTP_PROCEDURE_EXECUTION_STATISTICS'; + + SELECT DB_NAME() AS databaseName + ,DBScopedConfig = 'XTP_QUERY_EXECUTION_STATISTICS enabled:' + ,Status = CASE WHEN value = 1 THEN 'Yes' ELSE 'No' END + FROM sys.database_scoped_configurations + WHERE UPPER(name) = 'XTP_QUERY_EXECUTION_STATISTICS'; + END; + + /* + #################################################################################### + List any databases that are bound to resource pools + + NOTE #1: if there are memory optimized databases that do NOT appear + in this list, they consume memory from the 'default' pool, where + all other SQL Server memory is allocated from. + + If the memory-optimized footprint grows, from either addition of rows, + or row versions, it can put pressure on the buffer pool, cause it to shrink, + and affect performance for harddrive-based tables. + + NOTE #2: if you want to bind a memory-optimized database to resource pool, + the database must be taken OFFLINE/ONINE for the binding to take effect. + This will cause all durable data to be removed from memory, and re(streamed) + from checkpoint file pairs. + + #################################################################################### + */ + + IF EXISTS ( + SELECT 1 + FROM sys.databases d + INNER JOIN sys.dm_resource_governor_resource_pools AS Pools ON Pools.pool_id = d.resource_pool_id + ) + SELECT 'Resource pool' AS [object] + ,Pools.name AS poolName + ,d.name AS databaseName + ,min_memory_percent AS minMemoryPercent + ,max_memory_percent AS maxMemoryPercent + ,used_memory_kb / 1024 AS usedMemoryMB + ,max_memory_kb / 1024 AS maxMemoryMB + ,FORMAT(((used_memory_kb * 1.0) / (max_memory_kb * 1.0) * 100), '###.##') AS percentUsed + ,target_memory_kb / 1024 AS targetMemoryMB + FROM sys.databases d + INNER JOIN sys.dm_resource_governor_resource_pools AS Pools ON Pools.pool_id = d.resource_pool_id + ORDER BY poolName, databaseName; + + /* + ########################################################### + Memory breakdown + ########################################################### + */ + + ;WITH clerksAggregated AS + ( + SELECT clerks.[type] AS clerkType + ,CONVERT(CHAR(20) + ,SUM(clerks.pages_kb) / 1024.0) AS clerkTypeUsageMB + FROM sys.dm_os_memory_clerks AS clerks WITH (NOLOCK) + WHERE clerks.pages_kb <> 0 + AND clerks.type IN ('MEMORYCLERK_SQLBUFFERPOOL', 'MEMORYCLERK_XTP') + GROUP BY clerks.[type] + ) + ,clerksAggregatedString AS + ( + SELECT clerkType + ,clerkTypeUsageMB + ,PATINDEX('%.%', clerkTypeUsageMB) AS decimalPoint + FROM clerksAggregated + ) + SELECT clerkType + ,memUsageMB = + CASE + WHEN decimalPoint > 1 THEN SUBSTRING(clerkTypeUsageMB, 1, PATINDEX('%.%', clerkTypeUsageMB) -1) + ELSE clerkTypeUsageMB + END + FROM clerksAggregatedString; + + DECLARE @dm_os_memory_clerks TABLE + ( + clerk_type NVARCHAR(60) + ,name NVARCHAR(256) + ,memory_node_id SMALLINT + ,pages_mb BIGINT + ); + + INSERT @dm_os_memory_clerks + ( + clerk_type + ,name + ,memory_node_id + ,pages_mb + ) + -- total memory allocated for in-memory engine + SELECT type AS clerk_type + ,name + ,memory_node_id + ,pages_kb/1024 AS pages_mb + FROM sys.dm_os_memory_clerks + WHERE type LIKE '%xtp%'; + + IF EXISTS (SELECT 1 FROM @dm_os_memory_clerks) + SELECT * + FROM @dm_os_memory_clerks; + + + /* + ################################################################# + Oldest xtp transactions, they might prevent + garbage collection from cleaning up row versions + ################################################################# + */ + + DECLARE @dm_db_xtp_transactions TABLE + ( + [object] NVARCHAR(256) + ,xtp_transaction_id BIGINT + ,transaction_id BIGINT + ,session_id SMALLINT + ,begin_tsn BIGINT + ,end_tsn BIGINT + ,state_desc NVARCHAR(64) + ,result_desc NVARCHAR(64) + ); + + INSERT @dm_db_xtp_transactions + ( + object + ,xtp_transaction_id + ,transaction_id + ,session_id + ,begin_tsn + ,end_tsn + ,state_desc + ,result_desc + ) + SELECT TOP 10 'Oldest xtp transactions' AS [object] + ,xtp_transaction_id + ,transaction_id + ,session_id + ,begin_tsn + ,end_tsn + ,state_desc + ,result_desc + FROM sys.dm_db_xtp_transactions + ORDER BY begin_tsn DESC; + + IF EXISTS (SELECT 1 FROM @dm_db_xtp_transactions) + SELECT * + FROM @dm_db_xtp_transactions; + + /* + ################################################################# + Is event notification defined at the serverdb level? + If so, errors will be generated, as EN is not + supported for memory-optimized objects, and causes problems + ################################################################# + */ + + IF EXISTS( + SELECT 1 + FROM sys.event_notifications + ) + BEGIN + SELECT 'Event notifications are listed below'; + SELECT * + FROM sys.event_notifications; + END; + END; -- @instanceLevelOnly = 1 AND @MSSQLVersion >= 12 + + SELECT + 'Thanks for using sp_BlitzInMemoryOLTP!' AS [Thanks], + 'From Your Community Volunteers' AS [From], + 'http://FirstResponderKit.org' AS [At], + 'We hope you found this tool useful. Current version: ' + + @ScriptVersion + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.' AS [Version]; + + +END TRY + +BEGIN CATCH + THROW + PRINT 'Error: ' + CONVERT(varchar(50), ERROR_NUMBER()) + + ', Severity: ' + CONVERT(varchar(5), ERROR_SEVERITY()) + + ', State: ' + CONVERT(varchar(5), ERROR_STATE()) + + ', Procedure: ' + ISNULL(ERROR_PROCEDURE(), '-') + + ', Line: ' + CONVERT(varchar(5), ERROR_LINE()) + + ', User name: ' + CONVERT(sysname, CURRENT_USER); + PRINT ERROR_MESSAGE(); +END CATCH; +GO diff --git a/Deprecated/sp_BlitzIndex_SQL_Server_2005.sql b/Deprecated/sp_BlitzIndex_SQL_Server_2005.sql new file mode 100644 index 000000000..86d0f5845 --- /dev/null +++ b/Deprecated/sp_BlitzIndex_SQL_Server_2005.sql @@ -0,0 +1,2764 @@ +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 STATISTICS IO OFF; +SET STATISTICS TIME OFF; +GO + +USE master; +GO + +IF OBJECT_ID('dbo.sp_BlitzIndex') IS NOT NULL + DROP PROCEDURE dbo.sp_BlitzIndex; +GO + +CREATE PROCEDURE dbo.sp_BlitzIndex + @DatabaseName NVARCHAR(128) = null, /*Defaults to current DB if not specified*/ + @Mode tinyint=0, /*0=diagnose, 1=Summarize, 2=Index Usage Detail, 3=Missing Index Detail*/ + @SchemaName NVARCHAR(128) = NULL, /*Requires table_name as well.*/ + @TableName NVARCHAR(128) = NULL, /*Requires schema_name as well.*/ + /*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 */ + /*Note:@Filter doesn't do anything unless @Mode=0*/ +/* +sp_BlitzIndex(TM) v2.02 - Jan 30, 2014 + +(C) 2014, Brent Ozar Unlimited(TM). +See http://BrentOzar.com/go/eula for the End User Licensing Agreement. + +For help and how-to info, visit http://www.BrentOzar.com/BlitzIndex + +How to use: +-- Diagnose: + EXEC dbo.sp_BlitzIndex @DatabaseName='AdventureWorks'; +-- Return detail for a specific table: + EXEC dbo.sp_BlitzIndex @DatabaseName='AdventureWorks', @SchemaName='Person', @TableName='Person'; + +Known limitations of this version: + - Does not include FULLTEXT indexes. (A possibility in the future, let us know if you're interested.) + - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. + -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it's important for the user to understand if it's going to be offline and not just run a script. + -- Example 2: they do not include all the options the index may have been created with (padding, compression filegroup/partition scheme etc.) + -- (The compression and filegroup index create syntax isn't trivial because it's set at the partition level and isn't trivial to code. Two people have voted for wanting it so far.) + - Doesn't advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of problems.) + - Found something? Let us know at help@brentozar.com. + + Thanks for using sp_BlitzIndex(TM)! + Sincerely, + The Humans of Brent Ozar Unlimited(TM) + +CHANGE LOG (last five versions): + Jan 30, 2014 (v2.02) + Standardized calling parameters with sp_AskBrent(TM) and sp_BlitzIndex(TM). (@DatabaseName instead of @database_name, etc) + Added check_id 80 and 81-- what appear to be the most frequently used indexes (workaholics) + Added index_operational_stats info to table level output -- recent scans vs lookups + Broke index_usage_stats output into two categories, scans and lookups (also in table level output) + Changed db name, table name, index name to 128 length + Fixed findings_group column length in #BlitzIndexResults (fixed issues for users w/ longer db names) + Fixed issue where identities nearing end of range were only detected if the check was run with a specific db context + Fixed extra tab in @SchemaName= that made pasting into Excel awkward/wrong + Added abnormal psychology check for clustered columnstore indexes (and general support for detecting them) + Standardized underscores in create TSQL for missing indexes + Better error message when running in table mode and the table isn't found. + Added current timestamp to the header based on user request. (Didn't add startup time-- sorry! Too many things reset usage info, don't want to mislead anyone.) + Added fillfactor to index create statements. + Changed all index create statements to ONLINE=?, SORT_IN_TEMPDB=?. The user should decide at index create time what's right for them. + May 26, 2013 (v2.01) + Added check_id 28: Non-unqiue clustered indexes. (This should have been checked in for an earlier version, it slipped by). + May 14, 2013 (v2.0) - Added data types and max length to all columns (keys, includes, secret columns) + Set sp_blitz to default to current DB if database_name is not specified when called + Added @Filter: + 0=no filter (default) + 1=Don't throw low-usage warnings for objects with 0 reads (helpful for dev/non-production environments) + 2=Only report on objects >= 250MB (helps focus on larger indexes). Still runs a few database-wide checks as well. + Added list of all columns and types in table for runs using: @DatabaseName, @SchemaName, @TableName + Added count of total number of indexes a column is part of. + Added check_id 25: Addicted to nullable columns. (All or all but one column is nullable.) + Added check_id 66 and 67 to flag tables/indexes created within 1 week or modified within 48 hours. + Added check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes). + Added check_id 27: Addicted to strings. Looks for tables with 4 or more columns, of which all or all but one are string or LOB types. + Added check_id 68: Identity columns within 30% of the end of range (tinyint, smallint, int) AND + Negative identity seeds or identity increments <> 1 + Added check_id 69: Column collation does not match database collation + Added check_id 70: Replicated columns. This identifies which columns are in at least one replication publication. + Added check_id 71: Cascading updates or cascading deletes. + Split check_id 40 into two checks: fillfactor on nonclustered indexes < 80%, fillfactor on clustered indexes < 90% + Added check_id 33: Potential filtered indexes based on column names. + Fixed bug where you couldn't see detailed view for indexed views. + (Ex: EXEC dbo.sp_BlitzIndex @DatabaseName='AdventureWorks', @SchemaName='Production', @TableName='vProductAndDescription';) + Added four index usage columns to table detail output: last_user_seek, last_user_scan, last_user_lookup, last_user_update + Modified check_id 24. This now looks for wide clustered indexes (> 3 columns OR > 16 bytes). + Previously just simplistically looked for multiple column CX. + Removed extra spacing (non-breaking) in more_info column. + Fixed bug where create t-sql didn't include filter (for filtered indexes) + Fixed formatting bug where "magic number" in table detail view didn't have commas + Neatened up column names in result sets. + April 8, 2013 (v1.5) - Fixed breaking bug for partitioned tables with > 10(ish) partitions + Added schema_name to suggested create statement for PKs + Handled "magic_benefit_number" values for missing indexes >= 922,337,203,685,477 + Added count of NC indexes to Index Hoarder: Multi-column clustered index finding + Added link to EULA + Simplified aggressive index checks (blocking). Multiple checks confused people more than it helped. + Left only "Total lock wait time > 5 minutes (row + page)". + Added CheckId 25 for non-unique clustered indexes. + The "Create TSQL" column now shows a commented out drop command for disabled non-clustered indexes + Updated query which joins to sys.dm_operational_stats DMV when running against 2012 for performance reasons + December 20, 2012 (v1.4) - Fixed bugs for instances using a case-sensitive collation + Added support to identify compressed indexes + Added basic support for columnstore, XML, and spatial indexes + Added "Abnormal Psychology" diagnosis to alert you to special index types in a database + Removed hypothetical indexes and disabled indexes from "multiple personality disorders" + Fixed bug where hypothetical indexes weren't showing up in "self-loathing indexes" + Fixed bug where the partitioning key column was displayed in the key of aligned nonclustered indexes on partitioned tables + Added set options to the script so procedure is created with required settings for its use of computed columns + +*/ +AS + +SET NOCOUNT ON; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + +DECLARE @DatabaseID INT; +DECLARE @ObjectID INT; +DECLARE @dsql NVARCHAR(MAX); +DECLARE @params NVARCHAR(MAX); +DECLARE @msg NVARCHAR(4000); +DECLARE @ErrorSeverity INT; +DECLARE @ErrorState INT; +DECLARE @Rowcount BIGINT; +DECLARE @SQLServerProductVersion NVARCHAR(128); +DECLARE @SQLServerEdition INT; +DECLARE @FilterMB INT; +DECLARE @collation NVARCHAR(256); + + +SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); +SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ +SET @FilterMB=250; + +IF @DatabaseName is null + SET @DatabaseName=DB_NAME(); + +SELECT @DatabaseID = database_id +FROM sys.databases +WHERE [name] = @DatabaseName + AND user_access_desc='MULTI_USER' + AND state_desc = 'ONLINE'; + +---------------------------------------- +--STEP 1: OBSERVE THE PATIENT +--This step puts index information into temp tables. +---------------------------------------- +BEGIN TRY + BEGIN + + --Validate SQL Server Verson + + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 + )) <= 8 + BEGIN + SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2005 and higher. The version of this instance is: ' + @SQLServerProductVersion; + RAISERROR(@msg,16,1); + END + + --Short circuit here if database name does not exist. + IF @DatabaseName IS NULL OR @DatabaseID IS NULL + BEGIN + SET @msg='Database does not exist or is not online/multi-user: cannot proceed.' + RAISERROR(@msg,16,1); + END + + --Validate parameters. + IF (@Mode NOT IN (0,1,2,3)) + BEGIN + SET @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail'; + RAISERROR(@msg,16,1); + END + + IF (@Mode <> 0 AND @TableName IS NOT NULL) + BEGIN + SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; + RAISERROR(@msg,16,1); + END + + IF ((@Mode <> 0 OR @TableName IS NOT NULL) and @Filter <> 0) + BEGIN + SET @msg=N'@Filter only appies when @Mode=0 and @TableName is not specified. Please try again.'; + RAISERROR(@msg,16,1); + END + + IF (@SchemaName IS NOT NULL AND @TableName IS NULL) + BEGIN + SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.' + RAISERROR(@msg,16,1); + END + + + IF (@TableName IS NOT NULL AND @SchemaName IS NULL) + BEGIN + SET @SchemaName=N'dbo' + SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.' + RAISERROR(@msg,1,1) WITH NOWAIT; + END + + --If a table is specified, grab the object id. + --Short circuit if it doesn't exist. + IF @TableName IS NOT NULL + BEGIN + SET @dsql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @ObjectID= OBJECT_ID + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on + so.schema_id=sc.schema_id + where so.type in (''U'', ''V'') + and so.name=' + QUOTENAME(@TableName,'''')+ N' + and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' + /*Has a row in sys.indexes. This lets us get indexed views.*/ + and exists ( + SELECT si.name + FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si + WHERE so.object_id=si.object_id) + OPTION (RECOMPILE);'; + + SET @params='@ObjectID INT OUTPUT' + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; + + IF @ObjectID IS NULL + BEGIN + SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + + N'Please check your parameters.' + RAISERROR(@msg,1,1); + RETURN; + END + END + + RAISERROR(N'Starting run. sp_BlitzIndex(TM) v2.02 - Jan 30, 2014', 0,1) WITH NOWAIT; + + IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL + DROP TABLE #IndexSanity; + + IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL + DROP TABLE #IndexPartitionSanity; + + IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL + DROP TABLE #IndexSanitySize; + + IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL + DROP TABLE #IndexColumns; + + IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL + DROP TABLE #MissingIndexes; + + IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL + DROP TABLE #ForeignKeys; + + IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL + DROP TABLE #BlitzIndexResults; + + IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL + DROP TABLE #IndexCreateTsql; + + RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; + CREATE TABLE #BlitzIndexResults + ( + blitz_result_id INT IDENTITY PRIMARY KEY, + check_id INT NOT NULL, + index_sanity_id INT NULL, + findings_group VARCHAR(4000) NOT NULL, + finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NOT NULL, + details NVARCHAR(4000) NOT NULL, + index_definition NVARCHAR(MAX) NOT NULL, + secret_columns NVARCHAR(MAX) NULL, + index_usage_summary NVARCHAR(MAX) NULL, + index_size_summary NVARCHAR(MAX) NULL, + create_tsql NVARCHAR(MAX) NULL, + more_info NVARCHAR(MAX)NULL + ); + + CREATE TABLE #IndexSanity + ( + [index_sanity_id] INT IDENTITY PRIMARY KEY, + [database_id] SMALLINT NOT NULL , + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [index_type] TINYINT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [object_name] NVARCHAR(128) NOT NULL , + index_name NVARCHAR(128) NULL , + key_column_names NVARCHAR(MAX) NULL , + key_column_names_with_sort_order NVARCHAR(MAX) NULL , + key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , + count_key_columns INT NULL , + include_column_names NVARCHAR(MAX) NULL , + include_column_names_no_types NVARCHAR(MAX) NULL , + count_included_columns INT NULL , + partition_key_column_name NVARCHAR(MAX) NULL, + filter_definition NVARCHAR(MAX) NOT NULL , + is_indexed_view BIT NOT NULL , + is_unique BIT NOT NULL , + is_primary_key BIT NOT NULL , + is_XML BIT NOT NULL, + is_spatial BIT NOT NULL, + is_NC_columnstore BIT NOT NULL, + is_CX_columnstore BIT NOT NULL, + is_disabled BIT NOT NULL , + is_hypothetical BIT NOT NULL , + is_padded BIT NOT NULL , + fill_factor SMALLINT NOT NULL , + user_seeks BIGINT NOT NULL , + user_scans BIGINT NOT NULL , + user_lookups BIGINT NOT NULL , + user_updates BIGINT NULL , + last_user_seek DATETIME NULL , + last_user_scan DATETIME NULL , + last_user_lookup DATETIME NULL , + last_user_update DATETIME NULL , + is_referenced_by_foreign_key BIT DEFAULT(0), + secret_columns NVARCHAR(MAX) NULL, + count_secret_columns INT NULL, + create_date DATETIME NOT NULL, + modify_date DATETIME NOT NULL + ); + + CREATE TABLE #IndexPartitionSanity + ( + [index_partition_sanity_id] INT IDENTITY PRIMARY KEY , + [index_sanity_id] INT NULL , + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [partition_number] INT NOT NULL , + row_count BIGINT NOT NULL , + reserved_MB NUMERIC(29,2) NOT NULL , + reserved_LOB_MB NUMERIC(29,2) NOT NULL , + reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + leaf_insert_count BIGINT NULL , + leaf_delete_count BIGINT NULL , + leaf_update_count BIGINT NULL , + range_scan_count BIGINT NULL , + singleton_lookup_count BIGINT NULL , + forwarded_fetch_count BIGINT NULL , + lob_fetch_in_pages BIGINT NULL , + lob_fetch_in_bytes BIGINT NULL , + row_overflow_fetch_in_pages BIGINT NULL , + row_overflow_fetch_in_bytes BIGINT NULL , + row_lock_count BIGINT NULL , + row_lock_wait_count BIGINT NULL , + row_lock_wait_in_ms BIGINT NULL , + page_lock_count BIGINT NULL , + page_lock_wait_count BIGINT NULL , + page_lock_wait_in_ms BIGINT NULL , + index_lock_promotion_attempt_count BIGINT NULL , + index_lock_promotion_count BIGINT NULL, + data_compression_desc VARCHAR(60) NULL + ); + + CREATE TABLE #IndexSanitySize + ( + [index_sanity_size_id] INT IDENTITY NOT NULL , + [index_sanity_id] INT NOT NULL , + partition_count INT NOT NULL , + total_rows BIGINT NOT NULL , + total_reserved_MB NUMERIC(29,2) NOT NULL , + total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , + total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + total_leaf_delete_count BIGINT NULL, + total_leaf_update_count BIGINT NULL, + total_range_scan_count BIGINT NULL, + total_singleton_lookup_count BIGINT NULL, + total_forwarded_fetch_count BIGINT NULL, + total_row_lock_count BIGINT NULL , + total_row_lock_wait_count BIGINT NULL , + total_row_lock_wait_in_ms BIGINT NULL , + avg_row_lock_wait_in_ms BIGINT NULL , + total_page_lock_count BIGINT NULL , + total_page_lock_wait_count BIGINT NULL , + total_page_lock_wait_in_ms BIGINT NULL , + avg_page_lock_wait_in_ms BIGINT NULL , + total_index_lock_promotion_attempt_count BIGINT NULL , + total_index_lock_promotion_count BIGINT NULL , + data_compression_desc VARCHAR(8000) NULL + ); + + CREATE TABLE #IndexColumns + ( + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [key_ordinal] INT NULL , + is_included_column BIT NULL , + is_descending_key BIT NULL , + [partition_ordinal] INT NULL , + column_name NVARCHAR(256) NOT NULL , + system_type_name NVARCHAR(256) NOT NULL, + max_length SMALLINT NOT NULL, + [precision] TINYINT NOT NULL, + [scale] TINYINT NOT NULL, + collation_name NVARCHAR(256) NULL, + is_nullable bit NULL, + is_identity bit NULL, + is_computed bit NULL, + is_replicated bit NULL, + is_sparse bit NULL, + is_filestream bit NULL, + seed_value BIGINT NULL, + increment_value INT NULL , + last_value BIGINT NULL, + is_not_for_replication BIT NULL + ); + + CREATE TABLE #MissingIndexes + ([object_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [table_name] NVARCHAR(128), + [statement] NVARCHAR(512) NOT NULL, + magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), + avg_total_user_cost NUMERIC(29,1) NOT NULL, + avg_user_impact NUMERIC(29,1) NOT NULL, + user_seeks BIGINT NOT NULL, + user_scans BIGINT NOT NULL, + unique_compiles BIGINT NULL, + equality_columns NVARCHAR(4000), + inequality_columns NVARCHAR(4000), + included_columns NVARCHAR(4000) + ); + + CREATE TABLE #ForeignKeys ( + foreign_key_name NVARCHAR(256), + parent_object_id INT, + parent_object_name NVARCHAR(256), + referenced_object_id INT, + referenced_object_name NVARCHAR(256), + is_disabled BIT, + is_not_trusted BIT, + is_not_for_replication BIT, + parent_fk_columns NVARCHAR(MAX), + referenced_fk_columns NVARCHAR(MAX), + update_referential_action_desc NVARCHAR(16), + delete_referential_action_desc NVARCHAR(60) + ) + + CREATE TABLE #IndexCreateTsql ( + index_sanity_id INT NOT NULL, + create_tsql NVARCHAR(MAX) NOT NULL + ) + + --set @collation + SELECT @collation=collation_name + FROM sys.databases + where database_id=@DatabaseID; + + --insert columns for clustered indexes and heaps + --collect info on identity columns for this one + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + case when @SQLServerProductVersion not like '9%' THEN N'c.is_sparse' else N'NULL as is_sparse' END + N', + ' + case when @SQLServerProductVersion not like '9%' THEN N'c.is_filestream' else N'NULL as is_filestream' END + N', + CAST(ic.seed_value AS BIGINT), + CAST(ic.increment_value AS INT), + CAST(ic.last_value AS BIGINT), + ic.is_not_for_replication + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON + si.object_id=c.object_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON + c.object_id=ic.object_id and + c.column_id=ic.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + WHERE si.index_id in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N';'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; + INSERT #IndexColumns ( object_id, index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) + EXEC sp_executesql @dsql; + + --insert columns for nonclustered indexes + --this uses a full join to sys.index_columns + --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + case when @SQLServerProductVersion not like '9%' THEN N'c.is_sparse' else N'NULL AS is_sparse' END + N', + ' + case when @SQLServerProductVersion not like '9%' THEN N'c.is_filestream' else N'NULL AS is_filestream' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON + si.object_id=c.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + WHERE si.index_id not in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N';'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; + INSERT #IndexColumns ( object_id, index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream ) + EXEC sp_executesql @dsql; + + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, + so.object_id, + si.index_id, + si.type, + ' + QUOTENAME(@DatabaseName, '''') + ' AS database_name, + sc.NAME AS [schema_name], + so.name AS [object_name], + si.name AS [index_name], + CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, + si.is_unique, + si.is_primary_key, + CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, + 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, + si.is_disabled, + si.is_hypothetical, + si.is_padded, + si.fill_factor,' + + case when @SQLServerProductVersion not like '9%' THEN ' + CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition + ELSE '''' + END AS filter_definition' ELSE ''''' AS filter_definition' END + ' + , ISNULL(us.user_seeks, 0), ISNULL(us.user_scans, 0), + ISNULL(us.user_lookups, 0), ISNULL(us.user_updates, 0), us.last_user_seek, us.last_user_scan, + us.last_user_lookup, us.last_user_update, + so.create_date, so.modify_date + FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si WITH (NOLOCK) + JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas sc ON so.schema_id = sc.schema_id + 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)) + ' + WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) + /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + + CASE WHEN @TableName IS NOT NULL THEN ' and so.name=' + QUOTENAME(@TableName,'''') + ' ' ELSE '' END + + 'OPTION ( RECOMPILE ); + '; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; + 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_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, + is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, user_seeks, user_scans, + user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, + create_date, modify_date ) + EXEC sp_executesql @dsql; + + RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET key_column_names = D1.key_column_names + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' + AS col_definition + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') ,TYPE).value('.', 'varchar(max)'), 1, 1, '')) + ) D1 ( key_column_names ) + + RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET partition_key_column_name = D1.partition_key_column_name + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1,''))) D1 + ( partition_key_column_name ) + + RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key + WHEN 1 THEN N' DESC' + ELSE N'' + + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' + END AS col_definition + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order ) + + RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key + WHEN 1 THEN N' [DESC]' + ELSE N'' + END AS col_definition + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order_no_types ) + + RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET include_column_names = D3.include_column_names + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + ) D3 ( include_column_names ); + + RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET include_column_names_no_types = D3.include_column_names_no_types + FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + ) D3 ( include_column_names_no_types ); + + RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET count_included_columns = D4.count_included_columns, + count_key_columns = D4.count_key_columns + FROM #IndexSanity si + CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 + ELSE 0 + END) AS count_included_columns, + SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 + ELSE 0 + END) AS count_key_columns + FROM #IndexColumns c + WHERE c.object_id = si.object_id + AND c.index_id = si.index_id + ) AS D4 ( count_included_columns, count_key_columns ); + + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 + )) <> 11 --Anything other than 2012 + BEGIN + + RAISERROR (N'Using non-2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; + + --NOTE: we're joining to sys.dm_db_index_operational_stats differently than you might think (not using a cross apply) + --This is because of quirks prior to SQL Server 2012 and in 2014 with this DMV. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ps.object_id, + ps.index_id, + ps.partition_number, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + os.leaf_insert_count, + os.leaf_delete_count, + os.leaf_update_count, + os.range_scan_count, + os.singleton_lookup_count, + os.forwarded_fetch_count, + os.lob_fetch_in_pages, + os.lob_fetch_in_bytes, + os.row_overflow_fetch_in_pages, + os.row_overflow_fetch_in_bytes, + os.row_lock_count, + os.row_lock_wait_count, + os.row_lock_wait_in_ms, + os.page_lock_count, + os.page_lock_wait_count, + os.page_lock_wait_in_ms, + os.index_lock_promotion_attempt_count, + os.index_lock_promotion_count, + ' + case when @SQLServerProductVersion not like '9%' THEN 'par.data_compression_desc ' ELSE 'null as data_compression_desc' END + ' + FROM ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + '.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + LEFT JOIN ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_operational_stats(' + + CAST(@DatabaseID AS NVARCHAR(10)) + ', NULL, NULL,NULL) AS os ON + ps.object_id=os.object_id and ps.index_id=os.index_id and ps.partition_number=os.partition_number + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + ' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ); + '; + END + ELSE /* Otherwise use this syntax which takes advantage of OUTER APPLY on the os_partitions DMV. + This performs better on 2012 tables using 1000+ partitions. */ + BEGIN + RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; + + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ps.object_id, + ps.index_id, + ps.partition_number, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + os.leaf_insert_count, + os.leaf_delete_count, + os.leaf_update_count, + os.range_scan_count, + os.singleton_lookup_count, + os.forwarded_fetch_count, + os.lob_fetch_in_pages, + os.lob_fetch_in_bytes, + os.row_overflow_fetch_in_pages, + os.row_overflow_fetch_in_bytes, + os.row_lock_count, + os.row_lock_wait_count, + os.row_lock_wait_in_ms, + os.page_lock_count, + os.page_lock_wait_count, + os.page_lock_wait_in_ms, + os.index_lock_promotion_attempt_count, + os.index_lock_promotion_count, + ' + case when @SQLServerProductVersion not like '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' + + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ); + '; + + END + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; + insert #IndexPartitionSanity ( + [object_id], + index_id, + partition_number, + row_count, + reserved_MB, + reserved_LOB_MB, + reserved_row_overflow_MB, + leaf_insert_count, + leaf_delete_count, + leaf_update_count, + range_scan_count, + singleton_lookup_count, + forwarded_fetch_count, + lob_fetch_in_pages, + lob_fetch_in_bytes, + row_overflow_fetch_in_pages, + row_overflow_fetch_in_bytes, + row_lock_count, + row_lock_wait_count, + row_lock_wait_in_ms, + page_lock_count, + page_lock_wait_count, + page_lock_wait_in_ms, + index_lock_promotion_attempt_count, + index_lock_promotion_count, + data_compression_desc ) + EXEC sp_executesql @dsql; + + + RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; + UPDATE #IndexPartitionSanity + SET index_sanity_id = i.index_sanity_id + FROM #IndexPartitionSanity ps + JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] + AND ps.index_id = i.index_id + + + RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; + INSERT #IndexSanitySize ( [index_sanity_id], partition_count, total_rows, total_reserved_MB, + total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_range_scan_count, + total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, + total_forwarded_fetch_count,total_row_lock_count, + total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, + total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, + avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, + total_index_lock_promotion_count, data_compression_desc ) + SELECT index_sanity_id, COUNT(*), SUM(row_count), SUM(reserved_MB), SUM(reserved_LOB_MB), + SUM(reserved_row_overflow_MB), + SUM(range_scan_count), + SUM(singleton_lookup_count), + SUM(leaf_delete_count), + SUM(leaf_update_count), + SUM(forwarded_fetch_count), + SUM(row_lock_count), + SUM(row_lock_wait_count), + SUM(row_lock_wait_in_ms), + CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN + SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) + ELSE 0 END AS avg_row_lock_wait_in_ms, + SUM(page_lock_count), + SUM(page_lock_wait_count), + SUM(page_lock_wait_in_ms), + CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN + SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) + ELSE 0 END AS avg_page_lock_wait_in_ms, + SUM(index_lock_promotion_attempt_count), + SUM(index_lock_promotion_count), + LEFT(MAX(data_compression_info.data_compression_rollup),8000) + FROM #IndexPartitionSanity ipp + /* individual partitions can have distinct compression settings, just roll them into a list here*/ + OUTER APPLY (SELECT STUFF(( + SELECT N', ' + data_compression_desc + FROM #IndexPartitionSanity ipp2 + WHERE ipp.[object_id]=ipp2.[object_id] + AND ipp.[index_id]=ipp2.[index_id] + ORDER BY ipp2.partition_number + FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '')) + data_compression_info(data_compression_rollup) + GROUP BY index_sanity_id + ORDER BY index_sanity_id + OPTION ( RECOMPILE ); + + RAISERROR (N'Adding UQ index on #IndexSanity (object_id,index_id)',0,1) WITH NOWAIT; + CREATE UNIQUE INDEX uq_object_id_index_id ON #IndexSanity (object_id,index_id); + + RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; + SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT id.object_id, ' + QUOTENAME(@DatabaseName,'''') + N', sc.[name], so.[name], id.statement , gs.avg_total_user_cost, + gs.avg_user_impact, gs.user_seeks, gs.user_scans, gs.unique_compiles,id.equality_columns, + id.inequality_columns,id.included_columns + FROM sys.dm_db_missing_index_groups ig + JOIN sys.dm_db_missing_index_details id ON ig.index_handle = id.index_handle + JOIN sys.dm_db_missing_index_group_stats gs ON ig.index_group_handle = gs.group_handle + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so on + id.object_id=so.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc on + so.schema_id=sc.schema_id + WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + ' + ' + CASE WHEN @ObjectID IS NULL THEN N'' + ELSE N'and id.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + END + + N';' + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + INSERT #MissingIndexes ( [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, + avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, + inequality_columns,included_columns) + EXEC sp_executesql @dsql; + + SET @dsql = N' + SELECT + 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 + 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(''.'', ''varchar(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(''.'', ''varchar(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; + '; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; + INSERT #ForeignKeys ( foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, + is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, + [update_referential_action_desc], [delete_referential_action_desc] ) + EXEC sp_executesql @dsql; + + RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; + UPDATE #IndexSanity + SET is_referenced_by_foreign_key=1 + FROM #IndexSanity s + JOIN #ForeignKeys fk ON + s.object_id=fk.referenced_object_id + AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns + + RAISERROR (N'Add computed columns to #IndexSanity to simplify queries.',0,1) WITH NOWAIT; + ALTER TABLE #IndexSanity ADD + [schema_object_name] AS [schema_name] + '.' + [object_name] , + [schema_object_indexid] AS [schema_name] + '.' + [object_name] + + CASE WHEN [index_name] IS NOT NULL THEN '.' + index_name + ELSE '' + END + ' (' + CAST(index_id AS NVARCHAR(20)) + ')' , + first_key_column_name AS CASE WHEN count_key_columns > 1 + THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) + ELSE key_column_names + END , + index_definition AS + CASE WHEN partition_key_column_name IS NOT NULL + THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' + ELSE '' + END + + CASE index_id + WHEN 0 THEN N'[HEAP] ' + WHEN 1 THEN N'[CX] ' + ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN '[VIEW] ' + ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' + 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_disabled = 1 THEN N'[DISABLED] ' + ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' + ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 THEN N'[UNIQUE] ' + ELSE N'' END + CASE WHEN count_key_columns > 0 THEN + N'[' + CAST(count_key_columns AS VARCHAR(10)) + N' KEY' + + CASE WHEN count_key_columns > 1 then N'S' ELSE N'' END + + N'] ' + LTRIM(key_column_names_with_sort_order) + ELSE N'' END + CASE WHEN count_included_columns > 0 THEN + N' [' + CAST(count_included_columns AS VARCHAR(10)) + N' INCLUDE' + + + CASE WHEN count_included_columns > 1 then N'S' ELSE N'' END + + N'] ' + include_column_names + ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition + ELSE N'' END , + [total_reads] AS user_seeks + user_scans + user_lookups, + [reads_per_write] AS CAST(CASE WHEN user_updates > 0 + THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) + ELSE 0 END AS MONEY) , + [index_usage_summary] AS N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS money), 1), '.00', '') + + case when user_seeks + user_scans + user_lookups > 0 then + N' (' + + RTRIM( + CASE WHEN user_seeks > 0 then REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS money), 1), '.00', '') + N' seek ' ELSE N'' END + + CASE WHEN user_scans > 0 then REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS money), 1), '.00', '') + N' scan ' ELSE N'' END + + CASE WHEN user_lookups > 0 then REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS money), 1), '.00', '') + N' lookup' ELSE N'' END + ) + + N') ' + else N' ' end + + N'Writes:' + + REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS money), 1), '.00', ''), + [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + + N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([object_name],'''') + N';' + + RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; + UPDATE nc + SET secret_columns= + N'[' + + CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS VARCHAR(10)) END + + CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + + CASE WHEN tb.count_key_columns > 1 then N'S] ' ELSE N'] ' END + + CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + + /* Uniquifiers only needed on non-unique clustereds-- not heaps */ + CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END + END + , count_secret_columns= + CASE tb.index_id WHEN 0 THEN 1 ELSE + tb.count_key_columns + + CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END + END + FROM #IndexSanity AS nc + JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id + and tb.index_id in (0,1) + WHERE nc.index_id > 1; + + RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; + UPDATE tb + SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END + , count_secret_columns = 1 + FROM #IndexSanity AS tb + WHERE tb.index_id = 0 /*Heaps-- these have the RID */ + or (tb.index_id=1 and tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ + + RAISERROR (N'Add computed columns to #IndexSanitySize to simplify queries.',0,1) WITH NOWAIT; + ALTER TABLE #IndexSanitySize ADD + index_size_summary AS ISNULL( + CASE WHEN partition_count > 1 + THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' + ELSE N'' + END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS money), 1), N'.00', N'') + N' rows; ' + + CASE WHEN total_reserved_MB > 1024 THEN + CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' + ELSE + CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' + END + + CASE WHEN total_reserved_LOB_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB LOB' + WHEN total_reserved_LOB_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB LOB' + ELSE '' + END + + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' + WHEN total_reserved_row_overflow_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' + ELSE '' + END , + N'Error- NULL in computed column'), + index_op_stats AS ISNULL( + ( + REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' + + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN + REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' + ELSE N'' END + + /* rows will only be in this dmv when data is in memory for the table */ + ), N'Table metadata not in memory'), + index_lock_wait_summary AS ISNULL( + CASE WHEN total_row_lock_wait_count = 0 and total_page_lock_wait_count = 0 and + total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits.' + ELSE + CASE WHEN total_row_lock_wait_count > 0 THEN + N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS money), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS money), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS money), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS money), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS money), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_page_lock_wait_count > 0 THEN + N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS money), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS money), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS money), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS money), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS money), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN + N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS money), 1), N'.00', N'') + + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS money), 1), N'.00', N'') + N'.' + ELSE N'' + END + END + ,'Error- NULL in computed column') + + + RAISERROR (N'Add computed columns to #missing_index to simplify queries.',0,1) WITH NOWAIT; + ALTER TABLE #MissingIndexes ADD + [index_estimated_impact] AS + CAST(user_seeks + user_scans AS NVARCHAR(30)) + N' use' + + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END + +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) + + N'%; Avg query cost: ' + + CAST(avg_total_user_cost AS NVARCHAR(30)), + [missing_index_details] AS + CASE WHEN equality_columns IS NOT NULL THEN N'EQUALITY: ' + equality_columns + N' ' + ELSE N'' + END + CASE WHEN inequality_columns IS NOT NULL THEN N'INEQUALITY: ' + inequality_columns + N' ' + ELSE N'' + END + CASE WHEN included_columns IS NOT NULL THEN N'INCLUDES: ' + included_columns + N' ' + ELSE N'' + END, + [create_tsql] AS N'CREATE INDEX [ix_' + table_name + N'_' + + REPLACE(REPLACE(REPLACE(REPLACE( + ISNULL(equality_columns,N'')+ + CASE when equality_columns is not null and inequality_columns is not null then N'_' else N'' END + + ISNULL(inequality_columns,''),',','') + ,'[',''),']',''),' ','_') + + CASE WHEN included_columns IS NOT NULL THEN N'_includes' ELSE N'' END + N'] ON ' + + [statement] + N' (' + ISNULL(equality_columns,N'') + + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END + + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + + ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END + + N' WITH (' + + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?' + + N')' + + N';' + , + [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + + N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';' + ; + + + RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; + INSERT #IndexCreateTsql (index_sanity_id, create_tsql) + SELECT + index_sanity_id, + ISNULL ( + /* Script drops for disabled non-clustered indexes*/ + CASE WHEN is_disabled = 1 AND index_id <> 1 + THEN N'--DROP INDEX ' + QUOTENAME([index_name]) + N' ON ' + + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ELSE + CASE index_id WHEN 0 THEN N'--I''m a Heap!' + ELSE + CASE WHEN is_XML = 1 OR is_spatial=1 THEN N'' /* Not even trying for these just yet...*/ + ELSE + CASE WHEN is_primary_key=1 THEN + N'ALTER TABLE ' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] PRIMARY KEY ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_CX_columnstore= 1 THEN + N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + ELSE /*Else not a PK or cx columnstore */ + N'CREATE ' + + CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + + CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + + CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' + ELSE N'' END + + N'INDEX [' + + index_name + N'] ON ' + + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + + CASE WHEN is_NC_columnstore=1 THEN + N' (' + ISNULL(include_column_names_no_types,'') + N' )' + ELSE /*Else not colunnstore */ + N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' + + CASE WHEN include_column_names_no_types IS NOT NULL THEN + N' INCLUDE (' + include_column_names_no_types + N')' + ELSE N'' + END + END /*End non-colunnstore case */ + + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END + END /*End Non-PK index CASE */ + + CASE WHEN is_NC_columnstore=0 and is_CX_columnstore=0 then + N' WITH (' + + N'FILLFACTOR=' + CASE fill_factor when 0 then N'100' else CAST(fill_factor AS NVARCHAR(5)) END + ', ' + + N'ONLINE=?, SORT_IN_TEMPDB=?' + + N')' + else N'' end + + N';' + END /*End non-spatial and non-xml CASE */ + END + END, '[Unknown Error]') + AS create_tsql + FROM #IndexSanity; + + END +END TRY +BEGIN CATCH + RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL + BEGIN + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END + + SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; + + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; +END CATCH; + +---------------------------------------- +--STEP 2: DIAGNOSE THE PATIENT +--EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. +---------------------------------------- +BEGIN TRY +---------------------------------------- +--If @TableName is specified, just return information for that table. +--The @Mode parameter doesn't matter if you're looking at a specific table. +---------------------------------------- +IF @TableName IS NOT NULL +BEGIN + RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; + + --We do a left join here in case this is a disabled NC. + --In that case, it won't have any size info/pages allocated. + WITH table_mode_cte AS ( + SELECT + s.schema_object_indexid, + s.key_column_names, + s.index_definition, + ISNULL(s.secret_columns,N'') AS secret_columns, + s.fill_factor, + s.index_usage_summary, + sz.index_op_stats, + ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, + ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, + s.is_referenced_by_foreign_key, + (SELECT COUNT(*) + FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id + AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, + s.last_user_seek, + s.last_user_scan, + s.last_user_lookup, + s.last_user_update, + s.create_date, + s.modify_date, + ct.create_tsql, + 1 as display_order + FROM #IndexSanity s + LEFT JOIN #IndexSanitySize sz ON + s.index_sanity_id=sz.index_sanity_id + LEFT JOIN #IndexCreateTsql ct ON + s.index_sanity_id=ct.index_sanity_id + WHERE s.[object_id]=@ObjectID + UNION ALL + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + convert(nvarchar(16),getdate(),121) + + N' (sp_BlitzIndex(TM) v2.02 - Jan 30, 2014)' , + N'From Brent Ozar Unlimited(TM)' , + N'http://BrentOzar.com/BlitzIndex' , + N'Thanks from the Brent Ozar Unlimited(TM) team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.', + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + 0 as display_order + ) + SELECT + schema_object_indexid AS [Details: schema.table.index(indexid)], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + secret_columns AS [Secret Columns], + fill_factor AS [Fillfactor], + index_usage_summary AS [Usage Stats], + index_op_stats as [Op Stats], + index_size_summary AS [Size], + index_lock_wait_summary AS [Lock Waits], + is_referenced_by_foreign_key AS [Referenced by FK?], + FKs_covered_by_index AS [FK Covered by Index?], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update as [Last User Write], + create_date AS [Created], + modify_date AS [Last Modified], + create_tsql AS [Create TSQL] + FROM table_mode_cte + ORDER BY display_order ASC, key_column_names ASC + OPTION ( RECOMPILE ); + + IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL + BEGIN + SELECT N'Missing index.' AS Finding , + N'http://BrentOzar.com/go/Indexaphobia' AS URL , + mi.[statement] + ' Est Benefit: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(magic_benefit_number AS BIGINT) AS money), 1), '.00', '') + END AS [Estimated Benefit], + missing_index_details AS [Missing Index Request] , + index_estimated_impact AS [Estimated Impact], + create_tsql AS [Create TSQL] + FROM #MissingIndexes mi + WHERE [object_id] = @ObjectID + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); + END + ELSE + SELECT 'No missing indexes.' AS finding; + + SELECT + column_name AS [Column Name], + (SELECT COUNT(*) + FROM #IndexColumns c2 + WHERE c2.column_name=c.column_name + and c2.key_ordinal is not null) + + CASE WHEN c.index_id = 1 and c.key_ordinal is not null THEN + -1+ (SELECT COUNT(DISTINCT index_id) + from #IndexColumns c3 + where c3.index_id not in (0,1)) + ELSE 0 END + AS [Found In], + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name in (N'char',N'nchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length as NVARCHAR(20)) + N')' + WHEN system_type_name in (N'varchar',N'nvarchar') THEN N' (' + CAST(max_length/2 as NVARCHAR(20)) + N')' + ELSE '' + END + END + AS [Type], + CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], + max_length AS [Length (max bytes)], + [precision] AS [Prec], + [scale] AS [Scale], + CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], + CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], + CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], + CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], + CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], + collation_name AS [Collation] + FROM #IndexColumns AS c + where index_id in (0,1); + + IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL + BEGIN + SELECT parent_object_name + N': ' + foreign_key_name AS [Foreign Key], + parent_fk_columns AS [Foreign Key Columns], + referenced_object_name AS [Referenced Table], + referenced_fk_columns AS [Referenced Table Columns], + is_disabled AS [Is Disabled?], + is_not_trusted as [Not Trusted?], + is_not_for_replication [Not for Replication?], + [update_referential_action_desc] as [Cascading Updates?], + [delete_referential_action_desc] as [Cascading Deletes?] + FROM #ForeignKeys + ORDER BY [Foreign Key] + OPTION ( RECOMPILE ); + END + ELSE + SELECT 'No foreign keys.' AS finding; +END + +--If @TableName is NOT specified... +--Act based on the @Mode and @Filter. (@Filter applies only when @Mode=0 "diagnose") +ELSE +BEGIN; + IF @Mode=0 /* DIAGNOSE*/ + BEGIN; + RAISERROR(N'@Mode=0, we are diagnosing.', 0,1) WITH NOWAIT; + + RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 0 , + N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + convert(nvarchar(16),getdate(),121), + N'sp_BlitzIndex(TM) v2.02 - Jan 30, 2014' , + N'From Brent Ozar Unlimited(TM)' , N'http://BrentOzar.com/BlitzIndex' , + N'Thanks from the Brent Ozar Unlimited(TM) team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.' + , N'',N'' + ); + + ---------------------------------------- + --Multiple Index Personalities: Check_id 0-10 + ---------------------------------------- + BEGIN; + RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; + WITH duplicate_indexes + AS ( SELECT [object_id], key_column_names + FROM #IndexSanity + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical = 0 + AND is_disabled = 0 + GROUP BY [object_id], key_column_names + HAVING COUNT(*) > 1) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 1 AS check_id, + ip.index_sanity_id, + 'Multiple Index Personalities' AS findings_group, + 'Duplicate keys' AS finding, + N'http://BrentOzar.com/go/duplicateindex' AS URL, + ip.schema_object_indexid AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM duplicate_indexes di + JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] + AND ip.key_column_names = di.key_column_names + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + ORDER BY ip.object_id, ip.key_column_names_with_sort_order + OPTION ( RECOMPILE ); + + RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; + WITH borderline_duplicate_indexes + AS ( SELECT DISTINCT [object_id], first_key_column_name, key_column_names, + COUNT([object_id]) OVER ( PARTITION BY [object_id], first_key_column_name ) AS number_dupes + FROM #IndexSanity + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical=0 + AND is_disabled=0) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 2 AS check_id, + ip.index_sanity_id, + 'Multiple Index Personalities' AS findings_group, + 'Borderline duplicate keys' AS finding, + N'http://BrentOzar.com/go/duplicateindex' AS URL, + ip.schema_object_indexid AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM #IndexSanity AS ip + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + WHERE EXISTS ( + SELECT di.[object_id] + FROM borderline_duplicate_indexes AS di + WHERE di.[object_id] = ip.[object_id] AND + di.first_key_column_name = ip.first_key_column_name AND + di.key_column_names <> ip.key_column_names AND + di.number_dupes > 1 + ) + ORDER BY ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names + OPTION ( RECOMPILE ); + + END + ---------------------------------------- + --Aggressive Indexes: Check_id 10-19 + ---------------------------------------- + BEGIN; + + RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 11 AS check_id, + i.index_sanity_id, + N'Aggressive Indexes' AS findings_group, + N'Total lock wait time > 5 minutes (row + page)' AS finding, + N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, + i.schema_object_indexid + N': ' + + sz.index_lock_wait_summary AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 + OPTION ( RECOMPILE ); + END + + ---------------------------------------- + --Index Hoarder: Check_id 20-29 + ---------------------------------------- + BEGIN + RAISERROR(N'check_id 20: >=7 NC indexes on any given table. Yes, 7 is an arbitrary number.', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 20 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + 'Index Hoarder' AS findings_group, + 'Many NC indexes on a single table' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.schema_object_name AS details, + i.schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, + '' AS secret_columns, + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS money), 1), N'.00', N'') + N' reads (ALL); ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS money), 1), N'.00', N'') + N' writes (ALL); ', + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS money), 1), N'.00', N'') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id NOT IN ( 0, 1 ) + GROUP BY schema_object_name + HAVING COUNT(*) >= 7 + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + if @Filter = 1 /*@Filter=1 is "ignore unusued" */ + BEGIN + RAISERROR(N'Skipping checks on unused indexes (21 and 22) because @Filter=1', 0,1) WITH NOWAIT; + END + ELSE /*Otherwise, go ahead and do the checks*/ + BEGIN + RAISERROR(N'check_id 21: >=5 percent of indexes are unused. Yes, 5 is an arbitrary number.', 0,1) WITH NOWAIT; + DECLARE @percent_NC_indexes_unused NUMERIC(29,1); + DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); + + SELECT @percent_NC_indexes_unused =( 100.00 * SUM(CASE WHEN total_reads = 0 THEN 1 + ELSE 0 + END) ) / COUNT(*) , + @NC_indexes_unused_reserved_MB = SUM(CASE WHEN total_reads = 0 THEN sz.total_reserved_MB + ELSE 0 + END) + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id NOT IN ( 0, 1 ) + and i.is_unique = 0 + OPTION ( RECOMPILE ); + + IF @percent_NC_indexes_unused >= 5 + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 21 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + N'Index Hoarder' AS findings_group, + N'More than 5 percent NC indexes are unused' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + + N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, + i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, + '' AS secret_columns, + CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' + + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, + + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS money), 1), '.00', '') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id NOT IN ( 0, 1 ) + AND i.is_unique = 0 + AND total_reads = 0 + GROUP BY i.database_name + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 22: NC indexes with 0 reads. (Borderline)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 22 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Unused NC index' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'0 reads: ' + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.index_id NOT IN (0,1) /*NCs only*/ + and i.is_unique = 0 + ORDER BY i.schema_object_indexid + OPTION ( RECOMPILE ); + END /*end checks only run when @Filter <> 1*/ + + RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 23 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Borderline: Wide indexes (7 or more columns)' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' + + i.schema_object_indexid AS details, i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE ( count_key_columns + count_included_columns ) >= 7 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + SUM(CASE max_length when -1 THEN 0 ELSE max_length END) AS sum_max_length + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + and key_ordinal > 0 + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 24 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Wide clustered index (> 3 columns OR > 16 bytes)' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' + + CAST(cc.sum_max_length AS NVARCHAR(10)) + + N' bytes in clustered index:' + i.schema_object_name + + N'. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] AND i2.index_id <> 1 + AND i2.is_disabled=0 AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + WHERE index_id = 1 /* clustered only */ + AND + (count_key_columns > 3 /*More than three key columns.*/ + OR cc.sum_max_length > 15 /*More than 16 bytes in key */) + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 25: Addicted to nullable columns.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) as non_nullable_columns, + COUNT(*) as total_columns + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 25 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Addicted to nulls' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + i.schema_object_name + + N' allows null in ' + CAST((total_columns-non_nullable_columns) as NVARCHAR(10)) + + N' of ' + CAST(total_columns as NVARCHAR(10)) + + N' columns.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + WHERE i.index_id in (1,0) + AND cc.non_nullable_columns < 2 + and cc.total_columns > 3 + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + SUM(CASE max_length when -1 THEN 1 ELSE 0 END) AS count_lob_columns, + SUM(CASE max_length when -1 THEN 0 ELSE max_length END) AS sum_max_length, + COUNT(*) as total_columns + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 26 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Wide tables: 35+ cols or > 2000 non-LOB bytes' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + i.schema_object_name + + N' has ' + CAST((total_columns) as NVARCHAR(10)) + + N' total columns with a max possible width of ' + CAST(sum_max_length as NVARCHAR(10)) + + N' bytes.' + + CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) as NVARCHAR(10)) + + ' columns are LOB types.' ELSE '' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + WHERE i.index_id in (1,0) + and + (cc.total_columns >= 35 OR + cc.sum_max_length >= 2000) + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 27: Addicted to strings.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + SUM(CASE WHEN system_type_name in ('varchar','nvarchar','char') or max_length=-1 THEN 1 ELSE 0 END) as string_or_LOB_columns, + COUNT(*) as total_columns + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 27 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Addicted to strings' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + i.schema_object_name + + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) as NVARCHAR(10)) + + N' of ' + CAST(total_columns as NVARCHAR(10)) + + N' columns. Check if data types are valid.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 + WHERE i.index_id in (1,0) + AND calc1.non_string_or_lob_columns <= 1 + AND cc.total_columns > 3 + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 28: Non-unique clustered index.', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 28 AS check_id, + i.index_sanity_id, + N'Index Hoarder' AS findings_group, + N'Non-Unique clustered index' AS finding, + N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'Uniquifiers will be required! Clustered index: ' + i.schema_object_name + + N' and all NC indexes. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] AND i2.index_id <> 1 + AND i2.is_disabled=0 AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id = 1 /* clustered only */ + AND is_unique=0 /* not unique */ + AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + + END + ---------------------------------------- + --Feature-Phobic Indexes: Check_id 30-39 + ---------------------------------------- + BEGIN + RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; + + DECLARE @number_indexes_with_includes INT; + DECLARE @percent_indexes_with_includes NUMERIC(10, 1); + + SELECT @number_indexes_with_includes = SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END), + @percent_indexes_with_includes = 100.* + SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) + FROM #IndexSanity; + + IF @number_indexes_with_includes = 0 + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 30 AS check_id, + NULL AS index_sanity_id, + N'Feature-Phobic Indexes' AS findings_group, + N'No indexes use includes' AS finding, 'http://BrentOzar.com/go/IndexFeatures' AS URL, + N'No indexes use includes' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 0,1) WITH NOWAIT; + IF @percent_indexes_with_includes <= 3 AND @number_indexes_with_includes > 0 + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 31 AS check_id, + NULL AS index_sanity_id, + N'Feature-Phobic Indexes' AS findings_group, + N'Borderline: Includes are used in < 3% of indexes' AS findings, + N'http://BrentOzar.com/go/IndexFeatures' AS URL, + N'Only ' + CAST(@percent_indexes_with_includes AS NVARCHAR(10)) + '% of indexes have includes' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 32: filtered indexes and indexed views', 0,1) WITH NOWAIT; + DECLARE @count_filtered_indexes INT; + DECLARE @count_indexed_views INT; + + SELECT @count_filtered_indexes=COUNT(*) + FROM #IndexSanity + WHERE filter_definition <> '' OPTION ( RECOMPILE ); + + SELECT @count_indexed_views=COUNT(*) + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE is_indexed_view = 1 OPTION ( RECOMPILE ); + + IF @count_filtered_indexes = 0 AND @count_indexed_views=0 + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 32 AS check_id, + NULL AS index_sanity_id, + N'Feature-Phobic Indexes' AS findings_group, + N'Borderline: No filtered indexes or indexed views exist' AS finding, + N'http://BrentOzar.com/go/IndexFeatures' AS URL, + N'These are NOT always needed-- but do you know when you would use them?' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary OPTION ( RECOMPILE ); + END; + + RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 0,1) WITH NOWAIT; + + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 33 AS check_id, + i.index_sanity_id AS index_sanity_id, + N'Feature-Phobic Indexes' AS findings_group, + N'Potential filtered index (based on column name)' AS finding, + N'http://BrentOzar.com/go/IndexFeatures' AS URL, + N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexColumns ic + join #IndexSanity i on + ic.[object_id]=i.[object_id] and + ic.[index_id]=i.[index_id] and + i.[index_id] > 1 /* non-clustered index */ + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE column_name like 'is%' + or column_name like '%archive%' + or column_name like '%active%' + or column_name like '%flag%' + OPTION ( RECOMPILE ); + + ---------------------------------------- + --Self Loathing Indexes : Check_id 40-49 + ---------------------------------------- + BEGIN + + RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 40 AS check_id, + i.index_sanity_id, + N'Self Loathing Indexes' AS findings_group, + N'Low Fill Factor: nonclustered index' AS finding, + N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'Fill factor on ' + schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ + CASE WHEN (last_user_update is null OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates as NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id > 1 + and fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 40: Fillfactor in clustered 90 percent or less', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 40 AS check_id, + i.index_sanity_id, + N'Self Loathing Indexes' AS findings_group, + N'Low Fill Factor: clustered index' AS finding, + N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'Fill factor on ' + schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ + CASE WHEN (last_user_update is null OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates as NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id = 1 + and fill_factor BETWEEN 1 AND 90 OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 41: Hypothetical indexes ', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 41 AS check_id, + N'Self Loathing Indexes' AS findings_group, + N'Hypothetical Index' AS finding, 'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'Hypothetical Index: ' + schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + N'' AS index_usage_summary, + N'' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_hypothetical = 1 OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; + --Note: disabled NC indexes will have O rows in #IndexSanitySize! + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 42 AS check_id, + index_sanity_id, + N'Self Loathing Indexes' AS findings_group, + N'Disabled Index' AS finding, + N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'Disabled Index:' + schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + 'DISABLED' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_disabled = 1 OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 43: Heaps with forwarded records or deletes', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 43 AS check_id, + i.index_sanity_id, + N'Self Loathing Indexes' AS findings_group, + N'Heaps with forwarded records or deletes' AS finding, + N'http://BrentOzar.com/go/SelfLoathing' AS URL, + CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + ' forwarded fetches, ' + + CAST(h.leaf_delete_count AS NVARCHAR(256)) + ' deletes against heap:' + + schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 44: Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 44 AS check_id, + i.index_sanity_id, + N'Self Loathing Indexes' AS findings_group, + N'Active heap' AS finding, + N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND + (i.total_reads > 0 OR i.user_updates > 0) + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); + + + END; + ---------------------------------------- + --Indexaphobia + --Missing indexes with value >= 5 million: : Check_id 50-59 + ---------------------------------------- + BEGIN + RAISERROR(N'check_id 50: Indexaphobia.', 0,1) WITH NOWAIT; + WITH index_size_cte + AS ( SELECT i.[object_id], + MAX(i.index_sanity_id) AS index_sanity_id, + ISNULL ( + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) + AS NVARCHAR(30))+ N' NC indexes exist (' + + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 + THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. + AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' + ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + AS NVARCHAR(30)) + N'MB); ' + END + + CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS money), 1), '.00', '') + END + + + N' Estimated Rows;' + ,N'') AS index_size_summary + FROM #IndexSanity AS i + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + GROUP BY i.[object_id]) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary, create_tsql, more_info ) + SELECT 50 AS check_id, + sz.index_sanity_id, + N'Indexaphobia' AS findings_group, + N'High value missing index' AS finding, + N'http://BrentOzar.com/go/Indexaphobia' AS URL, + mi.[statement] + ' estimated benefit: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(magic_benefit_number AS BIGINT) AS money), 1), '.00', '') + END AS details, + missing_index_details AS [definition], + index_estimated_impact, + sz.index_size_summary, + mi.create_tsql, + mi.more_info + FROM #MissingIndexes mi + LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id + WHERE magic_benefit_number > 500000 + ORDER BY magic_benefit_number DESC; + + END + ---------------------------------------- + --Abnormal Psychology : Check_id 60-79 + ---------------------------------------- + BEGIN + RAISERROR(N'check_id 60: XML indexes', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 60 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'XML Indexes' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + N'' AS index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_XML = 1 OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 61: Columnstore indexes', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 61 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + CASE WHEN i.is_NC_columnstore=1 + THEN N'NC Columnstore Index' + ELSE N'Clustered Columnstore Index' + END AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_NC_columnstore = 1 OR i.is_CX_columnstore=1 + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 62: Spatial indexes', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 62 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Spatial indexes' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_spatial = 1 OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 63: Compressed indexes', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 63 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Compressed indexes' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 64: Partitioned', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 64 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Partitioned indexes' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.partition_key_column_name IS NOT NULL OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 65: Non-Aligned Partitioned', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 65 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Non-Aligned index on a partitioned table' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanity AS iParent ON + i.[object_id]=iParent.[object_id] + AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ + AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.partition_key_column_name IS NULL + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 66 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Recently created tables/indexes (1 week)' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid + N' was created on ' + + CONVERT(NVARCHAR(16),i.create_date,121) + + N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 67 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Recently modified tables/indexes (2 days)' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_indexid + N' was modified on ' + + CONVERT(NVARCHAR(16),i.modify_date,121) + + N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) + and /*Exclude recently created tables unless they've been modified after being created.*/ + (i.create_date < DATEADD(dd,-7,GETDATE()) or i.create_date <> i.modify_date) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; + -- Allowed Ranges: + --int -2,147,483,648 to 2,147,483,647 + --smallint -32,768 to 32,768 + --tinyint 0 to 255 + + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 68 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Identity column within ' + + CAST (calc1.percent_remaining as nvarchar(256)) + + N' percent end of range' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS money), 1), '.00', ''),N'NULL') + + N', seed of ' + + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS money), 1), '.00', ''),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic on + i.object_id=ic.object_id + and i.index_id in (0,1) /* heaps and cx only */ + and ic.is_identity=1 + and ic.system_type_name in ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + CROSS APPLY ( + SELECT CAST(CASE WHEN ic.increment_value >= 0 + THEN + CASE ic.system_type_name + WHEN 'int' then (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' then (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' then ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE 999 + END + ELSE --ic.increment_value is negative + CASE ic.system_type_name + WHEN 'int' then ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' then ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' then ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE -1 + END + END AS NUMERIC(5,1)) AS percent_remaining + ) as calc1 + WHERE i.index_id in (1,0) + and calc1.percent_remaining <= 30 + UNION ALL + SELECT 68 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Identity column using a negative seed or increment other than 1' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS money), 1), '.00', ''),N'NULL') + + N', seed of ' + + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS money), 1), '.00', ''),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic on + i.object_id=ic.object_id + and i.index_id in (0,1) /* heaps and cx only */ + and ic.is_identity=1 + and ic.system_type_name in ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE i.index_id in (1,0) + and (ic.seed_value < 0 or ic.increment_value <> 1) + ORDER BY finding, details DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + COUNT(*) as column_count + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + and collation_name <> @collation + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 69 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Column collation does not match database collation' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_name + + N' has ' + CAST(column_count AS NVARCHAR(20)) + + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END + + N' with a different collation than the db collation of ' + + @collation AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + WHERE i.index_id in (1,0) + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + COUNT(*) as column_count, + SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) as replicated_column_count + FROM #IndexColumns ic + WHERE index_id in (1,0) /*Heap or clustered only*/ + GROUP BY object_id + ) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 70 AS check_id, + i.index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Replicated columns' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + i.schema_object_name + + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) + + N' out of ' + CAST(column_count AS NVARCHAR(20)) + + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END + + N' in one or more publications.' + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + WHERE i.index_id in (1,0) + and replicated_column_count > 0 + ORDER BY i.schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary, more_info ) + SELECT 71 AS check_id, + null as index_sanity_id, + N'Abnormal Psychology' AS findings_group, + N'Cascading Updates or Deletes' AS finding, + N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + foreign_key_name + + N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' + + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' + + N' has settings:' + + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END + + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END + AS details, + N'N/A' + AS index_definition, + N'N/A' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary, + (SELECT TOP 1 more_info from #IndexSanity i where i.object_id=fk.parent_object_id) + AS more_info + from #ForeignKeys fk + where [delete_referential_action_desc] <> N'NO_ACTION' + OR [update_referential_action_desc] <> N'NO_ACTION' + + END + + ---------------------------------------- + --Workaholics: Check_id 80-89 + ---------------------------------------- + BEGIN + + RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + + --Workaholics according to index_usage_stats + --This isn't perfect: it mentions the number of scans present in a plan + --A "scan" isn't necessarily a full scan, but hey, we gotta do the best with what we've got. + --in the case of things like indexed views, the operator might be in the plan but never executed + SELECT TOP 5 + 80 AS check_id, + i.index_sanity_id as index_sanity_id, + N'Workaholics' as findings_group, + N'Scan-a-lots (index_usage_stats)' as finding, + N'http://BrentOzar.com/go/Workaholics' AS URL, + REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') + + N' scans against ' + i.schema_object_indexid + + N'. Latest scan: ' + ISNULL(cast(i.last_user_scan as nvarchar(128)),'?') + N'. ' + + N'ScanFactor=' + cast(((i.user_scans * iss.total_reserved_MB)/1000000.) as NVARCHAR(256)) as details, + isnull(i.key_column_names_with_sort_order,'N/A') as index_definition, + isnull(i.secret_columns,'') as secret_columns, + i.index_usage_summary as index_usage_summary, + iss.index_size_summary as index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize iss on i.index_sanity_id=iss.index_sanity_id + WHERE isnull(i.user_scans,0) > 0 + ORDER BY i.user_scans * iss.total_reserved_MB DESC; + + RAISERROR(N'check_id 81: Top recent accesses (op stats)', 0,1) WITH NOWAIT; + INSERT #BlitzIndexResults ( check_id, index_sanity_id, findings_group, finding, URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + --Workaholics according to index_operational_stats + --This isn't perfect either: range_scan_count contains full scans, partial scans, even seeks in nested loop ops + --But this can help bubble up some most-accessed tables + SELECT TOP 5 + 81 as check_id, + i.index_sanity_id as index_sanity_id, + N'Workaholics' as findings_group, + N'Top recent accesses (index_op_stats)' as finding, + N'http://BrentOzar.com/go/Workaholics' AS URL, + ISNULL(REPLACE( + CONVERT(NVARCHAR(50),cast((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), + N'.00',N'') + + N' uses of ' + i.schema_object_indexid + N'. ' + + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_range_scan_count AS MONEY),1),N'.00',N'') + N' scans or seeks. ' + + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups. ' + + N'OpStatsFactor=' + cast(((((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB))/1000000.) as varchar(256)),'') as details, + isnull(i.key_column_names_with_sort_order,'N/A') as index_definition, + isnull(i.secret_columns,'') as secret_columns, + i.index_usage_summary as index_usage_summary, + iss.index_size_summary as index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize iss on i.index_sanity_id=iss.index_sanity_id + WHERE isnull(iss.total_range_scan_count,0) > 0 or isnull(iss.total_singleton_lookup_count,0) > 0 + ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC; + + + END + + ---------------------------------------- + --FINISHING UP + ---------------------------------------- + BEGIN + INSERT #BlitzIndexResults ( check_id, findings_group, finding, URL, details, index_definition,secret_columns, + index_usage_summary, index_size_summary ) + VALUES ( 1000 , N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + convert(nvarchar(16),getdate(),121) , + N'' , N'http://www.BrentOzar.com/BlitzIndex' , + N'Thanks from the Brent Ozar Unlimited(TM), LLC team.', + N'We hope you found this tool useful.', + N'If you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.' + , N'',N'' + ); + + + END + RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; + + /*Return results.*/ + SELECT isnull(br.findings_group,N'') + + CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END + + br.finding AS [Finding], + br.URL, + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'') AS [More Info], + COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY [check_id] ASC, blitz_result_id ASC, findings_group; + + END; /* End @Mode=0 (diagnose)*/ + ELSE IF @Mode=1 /*Summarize*/ + BEGIN + --This mode is to give some overall stats on the database. + RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; + + SELECT + CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], + CAST(CAST(SUM(sz.total_reserved_MB)/ + 1024. AS numeric(29,1)) AS NVARCHAR(500)) AS [All GB], + CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS numeric(29,1)) AS NVARCHAR(500)) AS [LOB GB], + CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS numeric(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], + CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS numeric(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS numeric(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS numeric(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS numeric(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS numeric(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS numeric(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) as [Most Recent Modify Date], + 1 as [Display Order] + FROM #IndexSanity AS i + --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + UNION ALL + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + convert(nvarchar(16),getdate(),121) , + N'sp_BlitzIndex(TM) v2.02 - Jan 30, 2014' , + N'From Brent Ozar Unlimited(TM)' , + N'http://BrentOzar.com/BlitzIndex' , + N'Thanks from the Brent Ozar Unlimited(TM) team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.', + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,0 as display_order + ORDER BY [Display Order] ASC + OPTION (RECOMPILE); + + END /* End @Mode=1 (summarize)*/ + ELSE IF @Mode=2 /*Index Detail*/ + BEGIN + --This mode just spits out all the detail without filters. + --This supports slicing AND dicing in Excel + RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; + + SELECT database_name AS [Database Name], + [schema_name] AS [Schema Name], + [object_name] AS [Object Name], + ISNULL(index_name, '') AS [Index Name], + cast(index_id as VARCHAR(10))AS [Index ID], + schema_object_indexid AS [Details: schema.table.index(indexid)], + CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' + ELSE 'NonClustered' + END AS [Object Type], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], + ISNULL(count_key_columns, 0) AS [Count Key Columns], + ISNULL(include_column_names, '') AS [Include Column Names], + ISNULL(count_included_columns,0) AS [Count Included Columns], + ISNULL(secret_columns,'') AS [Secret Column Names], + ISNULL(count_secret_columns,0) AS [Count Secret Columns], + ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], + ISNULL(filter_definition, '') AS [Filter Definition], + is_indexed_view AS [Is Indexed View], + is_primary_key AS [Is Primary Key], + is_XML AS [Is XML], + is_spatial AS [Is Spatial], + is_NC_columnstore AS [Is NC Columnstore], + is_CX_columnstore AS [Is CX Columnstore], + is_disabled AS [Is Disabled], + is_hypothetical AS [Is Hypothetical], + is_padded AS [Is Padded], + fill_factor AS [Fill Factor], + is_referenced_by_foreign_key AS [Is Reference by Foreign Key], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Update], + total_reads AS [Total Reads], + user_updates AS [User Updates], + reads_per_write AS [Reads Per Write], + index_usage_summary AS [Index Usage], + sz.partition_count AS [Partition Count], + sz.total_rows AS [Rows], + sz.total_reserved_MB AS [Reserved MB], + sz.total_reserved_LOB_MB AS [Reserved LOB MB], + sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], + sz.index_size_summary AS [Index Size], + sz.total_row_lock_count AS [Row Lock Count], + sz.total_row_lock_wait_count AS [Row Lock Wait Count], + sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], + sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], + sz.total_page_lock_count AS [Page Lock Count], + sz.total_page_lock_wait_count AS [Page Lock Wait Count], + sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], + sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], + sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], + sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.data_compression_desc AS [Data Compression], + i.create_date AS [Create Date], + i.modify_date as [Modify Date], + more_info AS [More Info], + 1 as [Display Order] + FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + UNION ALL + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + convert(nvarchar(16),getdate(),121) + N'sp_BlitzIndex(TM) v2.02 - Jan 30, 2014' , + N'From Brent Ozar Unlimited(TM)' , + N'http://BrentOzar.com/BlitzIndex' , + N'Thanks from the Brent Ozar Unlimited(TM) team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.', + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL, NULL,NULL, NULL, NULL, NULL, NULL,NULL,NULL, + 0 as [Display Order] + ORDER BY [Display Order] ASC, [Reserved MB] DESC + OPTION (RECOMPILE); + + END /* End @Mode=2 (index detail)*/ + ELSE IF @Mode=3 /*Missing index Detail*/ + BEGIN + SELECT + database_name AS [Database], + [schema_name] AS [Schema], + table_name AS [Table], + CAST(magic_benefit_number AS BIGINT) + AS [Magic Benefit Number], + missing_index_details AS [Missing Index Details], + avg_total_user_cost AS [Avg Query Cost], + avg_user_impact AS [Est Index Improvement], + user_seeks AS [Seeks], + user_scans AS [Scans], + unique_compiles AS [Compiles], + equality_columns AS [Equality Columns], + inequality_columns AS [Inequality Columns], + included_columns AS [Included Columns], + index_estimated_impact AS [Estimated Impact], + create_tsql AS [Create TSQL], + more_info AS [More Info], + 1 as [Display Order] + FROM #MissingIndexes + UNION ALL + SELECT + N'sp_BlitzIndex(TM) v2.02 - Jan 30, 2014' , + N'From Brent Ozar Unlimited(TM)' , + N'http://BrentOzar.com/BlitzIndex' , + 100000000000, + N'Thanks from the Brent Ozar Unlimited(TM) team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.', + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL, 0 as display_order + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + + END /* End @Mode=3 (index detail)*/ +END +END TRY +BEGIN CATCH + RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; + + SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + + RAISERROR (@msg, + @ErrorSeverity, + @ErrorState + ); + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; + END CATCH; +GO + diff --git a/sp_BlitzQueryStore.sql b/Deprecated/sp_BlitzQueryStore.sql similarity index 69% rename from sp_BlitzQueryStore.sql rename to Deprecated/sp_BlitzQueryStore.sql index d2ab63384..45405097f 100644 --- a/sp_BlitzQueryStore.sql +++ b/Deprecated/sp_BlitzQueryStore.sql @@ -11,12 +11,12 @@ GO DECLARE @msg NVARCHAR(MAX) = N''; -- Must be a compatible, on-prem version of SQL (2016+) -IF ( (SELECT SERVERPROPERTY ('EDITION')) <> 'SQL Azure' +IF ( (SELECT CONVERT(NVARCHAR(128), SERVERPROPERTY ('EDITION'))) <> 'SQL Azure' AND (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) < 13 ) -- or Azure Database (not Azure Data Warehouse), running at database compat level 130+ -OR ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' - AND (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 +OR ( (SELECT CONVERT(NVARCHAR(128), SERVERPROPERTY ('EDITION'))) = 'SQL Azure' + AND (SELECT SERVERPROPERTY ('ENGINEEDITION')) NOT IN (5,8) AND (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 ) BEGIN @@ -31,31 +31,38 @@ GO ALTER PROCEDURE dbo.sp_BlitzQueryStore @Help BIT = 0, - @DatabaseName NVARCHAR(128) = NULL , + @DatabaseName NVARCHAR(128) = NULL, @Top INT = 3, @StartDate DATETIME2 = NULL, @EndDate DATETIME2 = NULL, @MinimumExecutionCount INT = NULL, - @DurationFilter DECIMAL(38,4) = NULL , + @DurationFilter DECIMAL(38,4) = NULL, @StoredProcName NVARCHAR(128) = NULL, @Failed BIT = 0, @PlanIdFilter INT = NULL, @QueryIdFilter INT = NULL, @ExportToExcel BIT = 0, - @HideSummary BIT = 0 , + @HideSummary BIT = 0, @SkipXML BIT = 0, @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @ExpertMode BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS BEGIN /*First BEGIN*/ SET NOCOUNT ON; +SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version NVARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; +SELECT @Version = '8.19', @VersionDate = '20240222'; +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + DECLARE /*Variables for the variable Gods*/ @msg NVARCHAR(MAX) = N'', --Used to format RAISERROR messages in some places @@ -91,25 +98,26 @@ FROM sys.configurations AS c WHERE c.name = N'min memory per query (KB)' OPTION (RECOMPILE); -/*Grabs log size for datbase*/ -SELECT @log_size_mb = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID(@DatabaseName) -AND mf.type_desc = 'LOG'; - -/*Grab avg tempdb file size*/ -SELECT @avg_tempdb_data_file = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID('tempdb') -AND mf.type_desc = 'ROWS'; - +/*Check if this is Azure first*/ +IF (SELECT CONVERT(NVARCHAR(128), SERVERPROPERTY ('EDITION'))) <> 'SQL Azure' + BEGIN + /*Grabs log size for datbase*/ + SELECT @log_size_mb = AVG(((mf.size * 8) / 1024.)) + FROM sys.master_files AS mf + WHERE mf.database_id = DB_ID(@DatabaseName) + AND mf.type_desc = 'LOG'; + + /*Grab avg tempdb file size*/ + SELECT @avg_tempdb_data_file = AVG(((mf.size * 8) / 1024.)) + FROM sys.master_files AS mf + WHERE mf.database_id = DB_ID('tempdb') + AND mf.type_desc = 'ROWS'; + END; /*Help section*/ IF @Help = 1 BEGIN - - SELECT N'You have requested assistance. It will arrive as soon as humanly possible.' AS [Take four red capsules, help is on the way]; PRINT N' sp_BlitzQueryStore from http://FirstResponderKit.org @@ -130,14 +138,10 @@ IF @Help = 1 Unknown limitations of this version: - Could be tickling - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - Copyright (c) 2016 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -157,16 +161,277 @@ IF @Help = 1 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. '; + /*Parameter info*/ + SELECT N'@Help' AS [Parameter Name] , + N'BIT' AS [Data Type] , + N'0' AS [Default Value], + N'Displays this help message.' AS [Parameter Description] + UNION ALL + SELECT N'@DatabaseName', + N'NVARCHAR(128)', + N'NULL', + N'The name of the database you want to check the query store for.' + UNION ALL + SELECT N'@Top', + N'INT', + N'3', + N'The number of records to retrieve and analyze from the query store. The following system views are used: query_store_query, query_context_settings, query_store_wait_stats, query_store_runtime_stats,query_store_plan.' + + UNION ALL + SELECT N'@StartDate', + N'DATETIME2(7)', + N'NULL', + N'Get query store info starting from this date. When not specified, sp_BlitzQueryStore gets info from the last 7 days' + UNION ALL + SELECT N'@EndDate', + N'DATETIME2(7)', + N'NULL', + N'Get query store info until this date. When not specified, sp_BlitzQueryStore gets info from the last 7 days' + UNION ALL + SELECT N'@MinimumExecutionCount', + N'INT', + N'NULL', + N'When a value is specified, sp_BlitzQueryStore gets info for queries where count_executions >= @MinimumExecutionCount' + UNION ALL + SELECT N'@DurationFilter', + N'DECIMAL(38,4)', + N'NULL', + N'Time unit - seconds. When a value is specified, sp_BlitzQueryStore gets info for queries where the average duration >= @DurationFilter' + UNION ALL + SELECT N'@StoredProcName', + N'NVARCHAR(128)', + N'NULL', + N'Get information for this specific stored procedure.' + UNION ALL + SELECT N'@Failed', + N'BIT', + N'0', + N'When set to 1, only information about failed queries is returned.' + UNION ALL + SELECT N'@PlanIdFilter', + N'INT', + N'NULL', + N'The ID of the plan you want to check for.' + UNION ALL + SELECT N'@QueryIdFilter', + N'INT', + N'NULL', + N'The ID of the query you want to check for.' + UNION ALL + SELECT N'@ExportToExcel', + N'BIT', + N'0', + N'When set to 1, prepares output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' + UNION ALL + SELECT N'@HideSummary', + N'BIT', + N'0', + N'When set to 1, hides the findings summary result set.' + UNION ALL + SELECT N'@SkipXML', + N'BIT', + N'0', + N'When set to 1, missing_indexes, implicit_conversion_info, cached_execution_parameters, are not returned. Does not affect query_plan_xml' + UNION ALL + SELECT N'@Debug', + N'BIT', + N'0', + N'Setting this to 1 will print dynamic SQL and select data from all tables used.' + UNION ALL + SELECT N'@ExpertMode', + N'BIT', + N'0', + N'When set to 1, more checks are done. Examples: many to many merge joins, row goals, adaptive joins, stats info, bad scans and plan forcing, computed columns that reference scalar UDFs.' + UNION ALL + SELECT N'@Version', + N'VARCHAR(30)', + N'NULL', + N'OUTPUT parameter holding version number.' + UNION ALL + SELECT N'@VersionDate', + N'DATETIME', + N'NULL', + N'OUTPUT parameter holding version date.' + UNION ALL + SELECT N'@VersionCheckMode', + N'BIT', + N'0', + N'Outputs the version number and date.' + + /* Column definitions */ + SELECT 'database_name' AS [Column Name], + 'NVARCHAR(258)' AS [Data Type], + 'The name of the database where the plan was encountered.' AS [Column Description] + UNION ALL + SELECT 'query_cost', + 'FLOAT', + 'The cost of the execution plan in query bucks.' + UNION ALL + SELECT 'plan_id', + 'BIGINT', + 'The ID of the plan from sys.query_store_plan.' + UNION ALL + SELECT 'query_id', + 'BIGINT', + 'The ID of the query from sys.query_store_query.' + UNION ALL + SELECT 'query_id_all_plan_ids', + 'VARCHAR(8000)', + 'Comma-separated list of all query plan IDs associated with this query.' + UNION ALL + SELECT 'query_sql_text', + 'NVARCHAR(MAX)', + 'The text of the query, as provided by the user/app. Includes whitespaces, hints and comments. Comments and spaces before and after the query text are ignored.' + UNION ALL + SELECT 'proc_or_function_name', + 'NVARCHAR(258)', + 'If the query is part of a function/stored procedure, you''ll see here the name of its parent object.' + UNION ALL + SELECT 'query_plan_xml', + ' XML', + 'The query plan. Click to display a graphical plan.' + UNION ALL + SELECT 'warnings', + 'VARCHAR(MAX)', + 'A list of individual warnings generated by this query.' + UNION ALL + SELECT 'pattern', + 'NVARCHAR(512)', + 'A list of performance related patterns identified for this query.' + UNION ALL + SELECT 'parameter_sniffing_symptoms', + 'NVARCHAR(4000)', + 'A list of all the identified symptoms that are usually indicators of parameter sniffing.' + UNION ALL + SELECT 'last_force_failure_reason_desc', + 'NVARCHAR(258)', + 'Reason why plan forcing failed. NONE if plan isn''t forced.' + UNION ALL + SELECT 'top_three_waits', + 'NVARCHAR(MAX)', + 'The top 3 wait types, and their times in milliseconds, recorded for this query.' + UNION ALL + SELECT 'missing_indexes', + 'XML', + 'Missing index recommendations retrieved from the query plan.' + UNION ALL + SELECT 'implicit_conversion_info', + 'XML', + 'Information about the implicit conversion warnings, if any, retrieved from the query plan.' + UNION ALL + SELECT 'cached_execution_parameters', + 'XML', + 'Names, data types, and values for the parameters used when the query plan was compiled.' + UNION ALL + SELECT 'count_executions ', + 'BIGINT', + 'The number of executions of this particular query.' + UNION ALL + SELECT 'count_compiles', + 'BIGINT', + 'The number of plan compilations for this particular query.' + UNION ALL + SELECT 'total_cpu_time', + 'BIGINT', + 'Total CPU time, reported in milliseconds, consumed by all executions of this query.' + UNION ALL + SELECT 'avg_cpu_time ', + 'BIGINT', + 'Average CPU time, reported in milliseconds, consumed by each execution of this query.' + UNION ALL + SELECT 'total_duration', + 'BIGINT', + 'Total elapsed time, reported in milliseconds, consumed by all executions of this query.' + UNION ALL + SELECT 'avg_duration', + 'BIGINT', + 'Average elapsed time, reported in milliseconds, consumed by each execution of this query.' + UNION ALL + SELECT 'total_logical_io_reads', + 'BIGINT', + 'Total logical reads, reported in MB, performed by this query.' + UNION ALL + SELECT 'avg_logical_io_reads', + 'BIGINT', + 'Average logical reads, reported in MB, performed by each execution of this query.' + UNION ALL + SELECT 'total_physical_io_reads', + 'BIGINT', + 'Total physical reads, reported in MB, performed by this query.' + UNION ALL + SELECT 'avg_physical_io_reads', + 'BIGINT', + 'Average physical reads, reported in MB, performed by each execution of this query.' + UNION ALL + SELECT 'total_logical_io_writes', + 'BIGINT', + 'Total logical writes, reported in MB, performed by this query.' + UNION ALL + SELECT 'avg_logical_io_writes', + 'BIGINT', + 'Average logical writes, reported in MB, performed by each execution of this query.' + UNION ALL + SELECT 'total_rowcount', + 'BIGINT', + 'Total number of rows returned for all executions of this query.' + UNION ALL + SELECT 'avg_rowcount', + 'BIGINT', + 'Average number of rows returned by each execution of this query.' + UNION ALL + SELECT 'total_query_max_used_memory', + 'DECIMAL(38,2)', + 'Total max memory grant, reported in MB, used by this query.' + UNION ALL + SELECT 'avg_query_max_used_memory', + 'DECIMAL(38,2)', + 'Average max memory grant, reported in MB, used by each execution of this query.' + UNION ALL + SELECT 'total_tempdb_space_used', + 'DECIMAL(38,2)', + 'Total tempdb space, reported in MB, used by this query.' + UNION ALL + SELECT 'avg_tempdb_space_used', + 'DECIMAL(38,2)', + 'Average tempdb space, reported in MB, used by each execution of this query.' + UNION ALL + SELECT 'total_log_bytes_used', + 'DECIMAL(38,2)', + 'Total number of bytes in the database log used by this query.' + UNION ALL + SELECT 'avg_log_bytes_used', + 'DECIMAL(38,2)', + 'Average number of bytes in the database log used by each execution of this query.' + UNION ALL + SELECT 'total_num_physical_io_reads', + 'DECIMAL(38,2)', + 'Total number of physical I/O reads performed by this query (expressed as a number of read I/O operations).' + UNION ALL + SELECT 'avg_num_physical_io_reads', + 'DECIMAL(38,2)', + 'Average number of physical I/O reads performed by each execution of this query (expressed as a number of read I/O operations).' + UNION ALL + SELECT 'first_execution_time', + 'DATETIME2', + 'First execution time for this query within the aggregation interval. This is the end time of the query execution.' + UNION ALL + SELECT 'last_execution_time', + 'DATETIME2', + 'Last execution time for this query within the aggregation interval. This is the end time of the query execution.' + UNION ALL + SELECT 'context_settings', + 'NVARCHAR(512)', + 'Contains information about context settings associated with this query.'; RETURN; END; -/*Making sure your version is copasetic*/ -IF ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' ) +/*Making sure your version is copacetic*/ +IF ( (SELECT CONVERT(NVARCHAR(128), SERVERPROPERTY ('EDITION'))) = 'SQL Azure' ) BEGIN SET @is_azure_db = 1; - IF ( (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 + IF ( (SELECT SERVERPROPERTY ('ENGINEEDITION')) NOT IN (5,8) OR (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 ) BEGIN @@ -188,23 +453,23 @@ IF ( SELECT COUNT(*) WHERE d.is_query_store_on = 1 AND d.user_access_desc='MULTI_USER' AND d.state_desc = 'ONLINE' - AND d.name NOT IN ('master', 'model', 'msdb', 'tempdb', '32767') + AND d.name NOT IN ('master', 'tempdb', '32767') AND d.is_distributor = 0 ) = 0 BEGIN SELECT @msg = N'You don''t currently have any databases with Query Store enabled.' + REPLICATE(CHAR(13), 7933); PRINT @msg; RETURN; END; - + /*Making sure your databases are using QDS.*/ RAISERROR('Checking database validity', 0, 1) WITH NOWAIT; -IF (@is_azure_db = 1) +IF (@is_azure_db = 1 AND SERVERPROPERTY ('ENGINEEDITION') <> 8) SET @DatabaseName = DB_NAME(); ELSE BEGIN - /*If we're on Azure we don't need to check all this @DatabaseName stuff...*/ + /*If we're on Azure SQL DB we don't need to check all this @DatabaseName stuff...*/ SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)); @@ -226,7 +491,7 @@ BEGIN /*Is it online?*/ RAISERROR('Making sure [%s] is online', 0, 1, @DatabaseName) WITH NOWAIT; - IF (DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' + IF (DATABASEPROPERTYEX(@DatabaseName, 'Collation')) IS NULL BEGIN RAISERROR('The @DatabaseName you specified ([%s]) is not readable. Please check the name and try again. Better yet, check your server.', 0, 1, @DatabaseName); RETURN; @@ -236,14 +501,14 @@ END; /*Does it have Query Store enabled?*/ RAISERROR('Making sure [%s] has Query Store enabled', 0, 1, @DatabaseName) WITH NOWAIT; IF - ((DB_ID(@DatabaseName)) IS NOT NULL AND @DatabaseName <> '') -AND - ( SELECT DB_NAME(d.database_id) - FROM sys.databases AS d - WHERE d.is_query_store_on = 1 - AND d.user_access_desc='MULTI_USER' - AND d.state_desc = 'ONLINE' - AND DB_NAME(d.database_id) = @DatabaseName ) IS NULL + + ( SELECT [d].[name] + FROM [sys].[databases] AS d + WHERE [d].[is_query_store_on] = 1 + AND [d].[user_access_desc]='MULTI_USER' + AND [d].[state_desc] = 'ONLINE' + AND [d].[database_id] = (SELECT database_id FROM sys.databases WHERE name = @DatabaseName) + ) IS NULL BEGIN RAISERROR('The @DatabaseName you specified ([%s]) does not have the Query Store enabled. Please check the name or settings, and try again.', 0, 1, @DatabaseName) WITH NOWAIT; RETURN; @@ -257,7 +522,7 @@ SELECT @compatibility_level = d.compatibility_level FROM sys.databases AS d WHERE d.name = @DatabaseName; -RAISERROR('The @DatabaseName you specified ([%s])is running in compatibility level ([%d]).', 0, 1, @DatabaseName, @compatibility_level) WITH NOWAIT; +RAISERROR('The @DatabaseName you specified ([%s]) is running in compatibility level ([%d]).', 0, 1, @DatabaseName, @compatibility_level) WITH NOWAIT; /*Making sure top is set to something if NULL*/ @@ -282,8 +547,8 @@ EXEC sys.sp_executesql @ws_sql, @ws_params, @i_out = @ws_out OUTPUT; SELECT @waitstats = CASE @ws_out WHEN 0 THEN 0 ELSE 1 END; SET @msg = N'Wait stats DMV ' + CASE @waitstats - WHEN 0 THEN N' does not exist, skipping.' - WHEN 1 THEN N' exists, will analyze.' + WHEN 0 THEN N'does not exist, skipping.' + WHEN 1 THEN N'exists, will analyze.' END; RAISERROR(@msg, 0, 1) WITH NOWAIT; @@ -319,11 +584,33 @@ EXEC sys.sp_executesql @nc_sql, @ws_params, @i_out = @nc_out OUTPUT; SELECT @new_columns = CASE @nc_out WHEN 12 THEN 1 ELSE 0 END; SET @msg = N'New query_store_runtime_stats columns ' + CASE @new_columns - WHEN 0 THEN N' do not exist, skipping.' - WHEN 1 THEN N' exist, will analyze.' + WHEN 0 THEN N'do not exist, skipping.' + WHEN 1 THEN N'exist, will analyze.' END; RAISERROR(@msg, 0, 1) WITH NOWAIT; +/* +This section determines if Parameter Sensitive Plan Optimization is enabled on SQL Server 2022+. +*/ + +RAISERROR('Checking for Parameter Sensitive Plan Optimization ', 0, 1) WITH NOWAIT; + +DECLARE @pspo_out BIT, + @pspo_enabled BIT, + @pspo_sql NVARCHAR(MAX) = N'SELECT @i_out = CONVERT(bit,dsc.value) + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.database_scoped_configurations dsc + WHERE dsc.name = ''PARAMETER_SENSITIVE_PLAN_OPTIMIZATION'';', + @pspo_params NVARCHAR(MAX) = N'@i_out INT OUTPUT'; + +EXEC sys.sp_executesql @pspo_sql, @pspo_params, @i_out = @pspo_out OUTPUT; + +SET @pspo_enabled = CASE WHEN @pspo_out = 1 THEN 1 ELSE 0 END; + +SET @msg = N'Parameter Sensitive Plan Optimization ' + CASE @pspo_enabled + WHEN 0 THEN N'not enabled, skipping.' + WHEN 1 THEN N'enabled, will analyze.' + END; +RAISERROR(@msg, 0, 1) WITH NOWAIT; /* These are the temp tables we use @@ -353,6 +640,14 @@ CREATE TABLE #grouped_interval total_count_executions BIGINT NULL, total_avg_log_bytes_mb DECIMAL(38, 2) NULL, total_avg_tempdb_space DECIMAL(38, 2) NULL, + total_max_duration_ms DECIMAL(38, 2) NULL, + total_max_cpu_time_ms DECIMAL(38, 2) NULL, + total_max_logical_io_reads_mb DECIMAL(38, 2) NULL, + total_max_physical_io_reads_mb DECIMAL(38, 2) NULL, + total_max_logical_io_writes_mb DECIMAL(38, 2) NULL, + total_max_query_max_used_memory_mb DECIMAL(38, 2) NULL, + total_max_log_bytes_mb DECIMAL(38, 2) NULL, + total_max_tempdb_space DECIMAL(38, 2) NULL, INDEX gi_ix_dates CLUSTERED (start_range, end_range) ); @@ -366,7 +661,7 @@ CREATE TABLE #working_plans ( plan_id BIGINT, query_id BIGINT, - pattern NVARCHAR(256), + pattern NVARCHAR(258), INDEX wp_ix_ids CLUSTERED (plan_id, query_id) ); @@ -378,14 +673,15 @@ DROP TABLE IF EXISTS #working_metrics; CREATE TABLE #working_metrics ( - database_name NVARCHAR(256), + database_name NVARCHAR(258), plan_id BIGINT, query_id BIGINT, + query_id_all_plan_ids VARCHAR(8000), /*these columns are from query_store_query*/ - proc_or_function_name NVARCHAR(256), + proc_or_function_name NVARCHAR(258), batch_sql_handle VARBINARY(64), query_hash BINARY(8), - query_parameterization_type_desc NVARCHAR(256), + query_parameterization_type_desc NVARCHAR(258), parameter_sniffing_symptoms NVARCHAR(4000), count_compiles BIGINT, avg_compile_duration DECIMAL(38,2), @@ -473,6 +769,7 @@ CREATE TABLE #working_metrics total_log_bytes_used AS avg_log_bytes_used * count_executions, total_tempdb_space_used AS avg_tempdb_space_used * count_executions, xpm AS NULLIF(count_executions, 0) / NULLIF(DATEDIFF(MINUTE, first_execution_time, last_execution_time), 0), + percent_memory_grant_used AS CONVERT(MONEY, ISNULL(NULLIF(( max_query_max_used_memory * 1.00 ), 0) / NULLIF(min_query_max_used_memory, 0), 0) * 100.), INDEX wm_ix_ids CLUSTERED (plan_id, query_id, query_hash) ); @@ -484,7 +781,7 @@ DROP TABLE IF EXISTS #working_plan_text; CREATE TABLE #working_plan_text ( - database_name NVARCHAR(256), + database_name NVARCHAR(258), plan_id BIGINT, query_id BIGINT, /*These are from query_store_plan*/ @@ -499,16 +796,13 @@ CREATE TABLE #working_plan_text is_forced_plan BIT, is_natively_compiled BIT, force_failure_count BIGINT, - last_force_failure_reason_desc NVARCHAR(256), + last_force_failure_reason_desc NVARCHAR(258), count_compiles BIGINT, initial_compile_start_time DATETIME2, last_compile_start_time DATETIME2, last_execution_time DATETIME2, avg_compile_duration DECIMAL(38,2), last_compile_duration BIGINT, - min_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - max_used_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - percent_memory_grant_used AS CONVERT(MONEY, ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100.), /*These are from query_store_query*/ query_sql_text NVARCHAR(MAX), statement_sql_handle VARBINARY(64), @@ -534,13 +828,15 @@ CREATE TABLE #working_warnings query_id BIGINT, query_hash BINARY(8), sql_handle VARBINARY(64), - proc_or_function_name NVARCHAR(256), + proc_or_function_name NVARCHAR(258), plan_multiple_plans BIT, is_forced_plan BIT, is_forced_parameterized BIT, is_cursor BIT, is_optimistic_cursor BIT, is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, is_parallel BIT, is_forced_serial BIT, is_key_lookup_expensive BIT, @@ -612,6 +908,12 @@ CREATE TABLE #working_warnings is_big_log BIT, is_big_tempdb BIT, is_paul_white_electric BIT, + is_row_goal BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + busy_loops BIT, + tvf_join BIT, implicit_conversion_info XML, cached_execution_parameters XML, missing_indexes XML, @@ -620,13 +922,14 @@ CREATE TABLE #working_warnings ); +/*This is where we store some wait metrics*/ DROP TABLE IF EXISTS #working_wait_stats; CREATE TABLE #working_wait_stats ( plan_id BIGINT, wait_category TINYINT, - wait_category_desc NVARCHAR(256), + wait_category_desc NVARCHAR(258), total_query_wait_time_ms BIGINT, avg_query_wait_time_ms DECIMAL(38, 2), last_query_wait_time_ms BIGINT, @@ -649,7 +952,7 @@ CREATE TABLE #working_wait_stats WHEN 13 THEN N'BROKER_% (but not BROKER_RECEIVE_WAITFOR)' WHEN 14 THEN N'LOGMGR, LOGBUFFER, LOGMGR_RESERVE_APPEND, LOGMGR_FLUSH, LOGMGR_PMM_LOG, CHKPT, WRITELOG' WHEN 15 THEN N'ASYNC_NETWORK_IO, NET_WAITFOR_PACKET, PROXY_NETWORK_IO, EXTERNAL_SCRIPT_NETWORK_IOF' - WHEN 16 THEN N'CXPACKET, EXCHANGE' + WHEN 16 THEN N'CXPACKET, EXCHANGE, CXCONSUMER' WHEN 17 THEN N'RESOURCE_SEMAPHORE, CMEMTHREAD, CMEMPARTITIONED, EE_PMOLOCK, MEMORY_ALLOCATION_EXT, RESERVED_MEMORY_ALLOCATION_EXT, MEMORY_GRANT_UPDATE' WHEN 18 THEN N'WAITFOR, WAIT_FOR_RESULTS, BROKER_RECEIVE_WAITFOR' WHEN 19 THEN N'TRACEWRITE, SQLTRACE_LOCK, SQLTRACE_FILE_BUFFER, SQLTRACE_FILE_WRITE_IO_COMPLETION, SQLTRACE_FILE_READ_IO_COMPLETION, SQLTRACE_PENDING_BUFFER_WRITERS, SQLTRACE_SHUTDOWN, QUERY_TRACEOUT, TRACE_EVTNOTIFF' @@ -658,13 +961,11 @@ CREATE TABLE #working_wait_stats WHEN 22 THEN N'SE_REPL_%, REPL_%, HADR_% (but not HADR_THROTTLE_LOG_RATE_GOVERNOR), PWAIT_HADR_%, REPLICA_WRITES, FCB_REPLICA_WRITE, FCB_REPLICA_READ, PWAIT_HADRSIM' WHEN 23 THEN N'LOG_RATE_GOVERNOR, POOL_LOG_RATE_GOVERNOR, HADR_THROTTLE_LOG_RATE_GOVERNOR, INSTANCE_LOG_RATE_GOVERNOR' END, - INDEX wws_ix_ids CLUSTERED ( plan_id) + INDEX wws_ix_ids CLUSTERED (plan_id) ); -/* -The next three tables hold plan XML parsed out to different degrees -*/ +/*The next seven tables hold plan XML parsed out to different degrees*/ DROP TABLE IF EXISTS #statements; CREATE TABLE #statements @@ -674,6 +975,7 @@ CREATE TABLE #statements query_hash BINARY(8), sql_handle VARBINARY(64), statement XML, + is_cursor BIT INDEX s_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) ); @@ -731,12 +1033,12 @@ CREATE TABLE #stats_agg ( sql_handle VARBINARY(64), last_update DATETIME2, - modification_count DECIMAL(38, 2), + modification_count BIGINT, sampling_percent DECIMAL(38, 2), - [statistics] NVARCHAR(256), - [table] NVARCHAR(256), - [schema] NVARCHAR(256), - [database] NVARCHAR(256), + [statistics] NVARCHAR(258), + [table] NVARCHAR(258), + [schema] NVARCHAR(258), + [database] NVARCHAR(258), INDEX sa_ix_ids CLUSTERED (sql_handle) ); @@ -752,6 +1054,7 @@ CREATE TABLE #trace_flags ); +/*This is where we store the user-facing meaning of each check*/ DROP TABLE IF EXISTS #warning_results; CREATE TABLE #warning_results @@ -772,30 +1075,31 @@ CREATE TABLE #stored_proc_info ( sql_handle VARBINARY(64), query_hash BINARY(8), - variable_name NVARCHAR(256), - variable_datatype NVARCHAR(256), - converted_column_name NVARCHAR(256), - compile_time_value NVARCHAR(4000), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + converted_column_name NVARCHAR(258), + compile_time_value NVARCHAR(258), proc_name NVARCHAR(1000), - column_name NVARCHAR(256), - converted_to NVARCHAR(256) + column_name NVARCHAR(4000), + converted_to NVARCHAR(258), + set_options NVARCHAR(1000) INDEX tf_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #variable_info +DROP TABLE IF EXISTS #variable_info; CREATE TABLE #variable_info ( query_hash BINARY(8), sql_handle VARBINARY(64), proc_name NVARCHAR(1000), - variable_name NVARCHAR(256), - variable_datatype NVARCHAR(256), - compile_time_value NVARCHAR(4000), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + compile_time_value NVARCHAR(258), INDEX vif_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #conversion_info +DROP TABLE IF EXISTS #conversion_info; CREATE TABLE #conversion_info ( @@ -816,20 +1120,21 @@ CREATE TABLE #conversion_info INDEX cif_ix_ids CLUSTERED (sql_handle, query_hash) ); -/* These tables support the Missing Index details clickable*/ +/* These tables make the Missing Index details clickable*/ -DROP TABLE IF EXISTS #missing_index_xml +DROP TABLE IF EXISTS #missing_index_xml; CREATE TABLE #missing_index_xml ( query_hash BINARY(8), sql_handle VARBINARY(64), impact FLOAT, - index_xml XML + index_xml XML, + INDEX mix_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #missing_index_schema +DROP TABLE IF EXISTS #missing_index_schema; CREATE TABLE #missing_index_schema ( @@ -839,11 +1144,12 @@ CREATE TABLE #missing_index_schema database_name NVARCHAR(128), schema_name NVARCHAR(128), table_name NVARCHAR(128), - index_xml XML + index_xml XML, + INDEX mis_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #missing_index_usage +DROP TABLE IF EXISTS #missing_index_usage; CREATE TABLE #missing_index_usage ( @@ -854,10 +1160,11 @@ CREATE TABLE #missing_index_usage schema_name NVARCHAR(128), table_name NVARCHAR(128), usage NVARCHAR(128), - index_xml XML + index_xml XML, + INDEX miu_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #missing_index_detail +DROP TABLE IF EXISTS #missing_index_detail; CREATE TABLE #missing_index_detail ( @@ -868,11 +1175,12 @@ CREATE TABLE #missing_index_detail schema_name NVARCHAR(128), table_name NVARCHAR(128), usage NVARCHAR(128), - column_name NVARCHAR(128) + column_name NVARCHAR(128), + INDEX mid_ix_ids CLUSTERED (sql_handle, query_hash) ); -DROP TABLE IF EXISTS #missing_index_pretty +DROP TABLE IF EXISTS #missing_index_pretty; CREATE TABLE #missing_index_pretty ( @@ -882,12 +1190,17 @@ CREATE TABLE #missing_index_pretty database_name NVARCHAR(128), schema_name NVARCHAR(128), table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + is_spool BIT, details AS N'/* ' + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' + + CASE is_spool + WHEN 0 + THEN N'The Query Processor estimates that implementing the ' + ELSE N'We estimate that implementing the ' + END + CONVERT(NVARCHAR(30), impact) + '%.' + CHAR(10) @@ -903,7 +1216,7 @@ CREATE TABLE #missing_index_pretty + N'CREATE NONCLUSTERED INDEX ix_' + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END + + CASE WHEN [include] IS NOT NULL THEN + N'_Includes' ELSE N'' END + CHAR(10) + N' ON ' + schema_name @@ -921,15 +1234,33 @@ CREATE TABLE #missing_index_pretty + N')' + CHAR(10) + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' + THEN N'INCLUDE (' + include + N') WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + ELSE N' WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' END + CHAR(10) + N'GO' + CHAR(10) - + N'*/' + + N'*/', + INDEX mip_ix_ids CLUSTERED (sql_handle, query_hash) +); + +DROP TABLE IF EXISTS #index_spool_ugly; + +CREATE TABLE #index_spool_ugly +( + query_hash BINARY(8), + sql_handle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + INDEX isu_ix_ids CLUSTERED (sql_handle, query_hash) ); + /*Sets up WHERE clause that gets used quite a bit*/ --Date stuff @@ -983,10 +1314,33 @@ IF @MinimumExecutionCount IS NOT NULL --You care about stored proc names IF @StoredProcName IS NOT NULL - BEGIN - RAISERROR(N'Setting stored proc filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName - '; + BEGIN + + IF (@pspo_enabled = 1) + BEGIN + RAISERROR(N'Setting stored proc filter, PSPO enabled', 0, 1) WITH NOWAIT; + /* If PSPO is enabled, the object_id for a variant query would be 0. To include it, we check whether the object_id = 0 query + is a variant query, and whether it's parent query belongs to @sp_StoredProcName. */ + SET @sql_where += N' AND (object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName + OR (qsq.object_id = 0 + AND EXISTS( + SELECT 1 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_variant vr + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query pqsq + ON pqsq.query_id = vr.parent_query_id + WHERE + vr.query_variant_query_id = qsq.query_id + AND object_name(pqsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName + ) + )) + '; + END + ELSE + BEGIN + RAISERROR(N'Setting stored proc filter', 0, 1) WITH NOWAIT; + SET @sql_where += N' AND object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName + '; + END END; --I will always love you, but hopefully this query will eventually end @@ -1026,8 +1380,6 @@ IF (@QueryIdFilter IS NOT NULL) '; END; - - IF @Debug = 1 RAISERROR(N'Starting WHERE clause:', 0, 1) WITH NOWAIT; PRINT @sql_where; @@ -1044,7 +1396,6 @@ IF (@ExportToExcel = 1 OR @SkipXML = 1) SET @HideSummary = 1; END; - IF @StoredProcName IS NOT NULL BEGIN @@ -1118,23 +1469,33 @@ SELECT CONVERT(DATE, qsrs.last_execution_time) AS flat_date, SUM((qsrs.avg_logical_io_reads * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_reads_mb, SUM((qsrs.avg_physical_io_reads* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_physical_io_reads_mb, SUM((qsrs.avg_logical_io_writes* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_writes_mb, - SUM(( qsrs.avg_query_max_used_memory * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_query_max_used_memory_mb, + SUM((qsrs.avg_query_max_used_memory * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_query_max_used_memory_mb, SUM(qsrs.avg_rowcount) AS total_rowcount, - SUM(qsrs.count_executions) AS total_count_executions' + SUM(qsrs.count_executions) AS total_count_executions, + SUM(qsrs.max_duration / 1000.) AS total_max_duration_ms, + SUM(qsrs.max_cpu_time / 1000.) AS total_max_cpu_time_ms, + SUM((qsrs.max_logical_io_reads * 8 ) / 1024.) AS total_max_logical_io_reads_mb, + SUM((qsrs.max_physical_io_reads* 8 ) / 1024.) AS total_max_physical_io_reads_mb, + SUM((qsrs.max_logical_io_writes* 8 ) / 1024.) AS total_max_logical_io_writes_mb, + SUM((qsrs.max_query_max_used_memory * 8 ) / 1024.) AS total_max_query_max_used_memory_mb '; IF @new_columns = 1 BEGIN SET @sql_select += N', SUM((qsrs.avg_log_bytes_used) / 1048576.) / SUM(qsrs.count_executions) AS total_avg_log_bytes_mb, - SUM(avg_tempdb_space_used) / SUM(qsrs.count_executions) AS total_avg_tempdb_space - ' - END + SUM(qsrs.avg_tempdb_space_used) / SUM(qsrs.count_executions) AS total_avg_tempdb_space, + SUM((qsrs.max_log_bytes_used) / 1048576.) AS total_max_log_bytes_mb, + SUM(qsrs.max_tempdb_space_used) AS total_max_tempdb_space + '; + END; IF @new_columns = 0 BEGIN SET @sql_select += N', NULL AS total_avg_log_bytes_mb, - NULL AS total_avg_tempdb_space - ' - END + NULL AS total_avg_tempdb_space, + NULL AS total_max_log_bytes_mb, + NULL AS total_max_tempdb_space + '; + END; SET @sql_select += N'FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs @@ -1168,7 +1529,9 @@ INSERT #grouped_interval WITH (TABLOCK) ( flat_date, start_range, end_range, total_avg_duration_ms, total_avg_cpu_time_ms, total_avg_logical_io_reads_mb, total_avg_physical_io_reads_mb, total_avg_logical_io_writes_mb, total_avg_query_max_used_memory_mb, total_rowcount, - total_count_executions, total_avg_log_bytes_mb, total_avg_tempdb_space ) + total_count_executions, total_max_duration_ms, total_max_cpu_time_ms, total_max_logical_io_reads_mb, + total_max_physical_io_reads_mb, total_max_logical_io_writes_mb, total_max_query_max_used_memory_mb, + total_avg_log_bytes_mb, total_avg_tempdb_space, total_max_log_bytes_mb, total_max_tempdb_space ) EXEC sys.sp_executesql @stmt = @sql_select, @params = @sp_params, @@ -1192,7 +1555,7 @@ RAISERROR(N'Gathering longest duration plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH duration_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1200,7 +1563,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''duration'' + qsp.plan_id, qsp.query_id, ''avg duration'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN duration_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1236,6 +1599,52 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH duration_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_duration_ms DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max duration'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN duration_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_duration DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + /*Get longest cpu plans*/ @@ -1244,7 +1653,7 @@ RAISERROR(N'Gathering highest cpu plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH cpu_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1252,7 +1661,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''cpu'' + qsp.plan_id, qsp.query_id, ''avg cpu'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN cpu_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1288,6 +1697,52 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH cpu_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_cpu_time_ms DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max cpu'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN cpu_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_cpu_time DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + /*Get highest logical read plans*/ @@ -1296,7 +1751,7 @@ RAISERROR(N'Gathering highest logical read plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH logical_reads_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1304,7 +1759,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''logical reads'' + qsp.plan_id, qsp.query_id, ''avg logical reads'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN logical_reads_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1340,6 +1795,52 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH logical_reads_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_logical_io_reads_mb DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max logical reads'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN logical_reads_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_logical_io_reads DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + /*Get highest physical read plans*/ @@ -1348,7 +1849,7 @@ RAISERROR(N'Gathering highest physical read plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH physical_read_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1356,7 +1857,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''physical reads'' + qsp.plan_id, qsp.query_id, ''avg physical reads'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN physical_read_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1392,6 +1893,52 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH physical_read_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_physical_io_reads_mb DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max physical reads'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN physical_read_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_physical_io_reads DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + /*Get highest logical write plans*/ @@ -1400,7 +1947,7 @@ RAISERROR(N'Gathering highest write plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH logical_writes_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1408,7 +1955,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''writes'' + qsp.plan_id, qsp.query_id, ''avg writes'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN logical_writes_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1444,6 +1991,52 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH logical_writes_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_logical_io_writes_mb DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max writes'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN logical_writes_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_logical_io_writes DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + /*Get highest memory use plans*/ @@ -1452,7 +2045,7 @@ RAISERROR(N'Gathering highest memory use plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH memory_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1460,7 +2053,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''memory'' + qsp.plan_id, qsp.query_id, ''avg memory'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN memory_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1495,6 +2088,51 @@ EXEC sys.sp_executesql @stmt = @sql_select, @params = @sp_params, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH memory_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_query_max_used_memory_mb DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max memory'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN memory_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_query_max_used_memory DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; /*Get highest row count plans*/ @@ -1504,7 +2142,7 @@ RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH rowcount_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1512,7 +2150,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''rows'' + qsp.plan_id, qsp.query_id, ''avg rows'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN rowcount_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1555,12 +2193,12 @@ RAISERROR(N'Gathering new 2017 new column info...', 0, 1) WITH NOWAIT; /*Get highest log byte count plans*/ -RAISERROR(N'Gathering highest log byte use plans', 0, 1) WITH NOWAIT; +RAISERROR(N'Gathering highest log byte use plans by average log bytes mb', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH rowcount_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1568,7 +2206,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''log bytes'' + qsp.plan_id, qsp.query_id, ''avg log bytes'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN rowcount_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1603,14 +2241,63 @@ EXEC sys.sp_executesql @stmt = @sql_select, @params = @sp_params, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; -/*Get highest row count plans*/ +RAISERROR(N'Gathering highest log byte use plans by max log bytes mb', 0, 1) WITH NOWAIT; -RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH rowcount_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_log_bytes_mb DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max log bytes'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN rowcount_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_log_bytes_used DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + + +/*Get highest tempdb use plans*/ + +RAISERROR(N'Gathering highest tempdb use plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' WITH rowcount_max -AS ( SELECT TOP 1 +AS ( SELECT TOP (1) gi.start_range, gi.end_range FROM #grouped_interval AS gi @@ -1618,7 +2305,7 @@ AS ( SELECT TOP 1 INSERT #working_plans WITH (TABLOCK) ( plan_id, query_id, pattern ) SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''tempdb space'' + qsp.plan_id, qsp.query_id, ''avg tempdb space'' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp JOIN rowcount_max AS dm ON qsp.last_execution_time >= dm.start_range @@ -1653,6 +2340,53 @@ EXEC sys.sp_executesql @stmt = @sql_select, @params = @sp_params, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' +WITH rowcount_max +AS ( SELECT TOP (1) + gi.start_range, + gi.end_range + FROM #grouped_interval AS gi + ORDER BY gi.total_max_tempdb_space DESC ) +INSERT #working_plans WITH (TABLOCK) + ( plan_id, query_id, pattern ) +SELECT TOP ( @sp_Top ) + qsp.plan_id, qsp.query_id, ''max tempdb space'' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp +JOIN rowcount_max AS dm +ON qsp.last_execution_time >= dm.start_range + AND qsp.last_execution_time < dm.end_range +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs +ON qsrs.plan_id = qsp.plan_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq +ON qsq.query_id = qsp.query_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt +ON qsqt.query_text_id = qsq.query_text_id +WHERE 1 = 1 + AND qsq.is_internal_query = 0 + AND qsp.query_plan IS NOT NULL + '; + +SET @sql_select += @sql_where; + +SET @sql_select += N'ORDER BY qsrs.max_tempdb_space_used DESC + OPTION (RECOMPILE); + '; + +IF @Debug = 1 + PRINT @sql_select; + +IF @sql_select IS NULL + BEGIN + RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RETURN; + END; + +EXEC sys.sp_executesql @stmt = @sql_select, + @params = @sp_params, + @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + + END; @@ -1840,12 +2574,55 @@ EXEC sys.sp_executesql @stmt = @sql_select, @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; +/*If PSPO is enabled, get procedure names for variant queries.*/ +IF (@pspo_enabled = 1) +BEGIN + DECLARE + @pspo_names NVARCHAR(MAX) = ''; + + SET @pspo_names = + 'UPDATE wm + SET + wm.proc_or_function_name = + QUOTENAME(object_schema_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) + ''.'' + + QUOTENAME(object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) + FROM #working_metrics wm + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_variant AS vr + ON vr.query_variant_query_id = wm.query_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq + ON qsq.query_id = vr.parent_query_id + AND qsq.object_id > 0 + WHERE + wm.proc_or_function_name IS NULL;' + + EXEC sys.sp_executesql @pspo_names; +END; + /*This just helps us classify our queries*/ UPDATE #working_metrics SET proc_or_function_name = N'Statement' -WHERE proc_or_function_name IS NULL; +WHERE proc_or_function_name IS NULL +OPTION(RECOMPILE); + +SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; +SET @sql_select += N' + WITH patterns AS ( + SELECT query_id, planid_path = STUFF((SELECT DISTINCT N'', '' + RTRIM(qsp2.plan_id) + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp2 + WHERE qsp.query_id = qsp2.query_id + FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 2, N'''') + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp + ) + UPDATE wm + SET wm.query_id_all_plan_ids = patterns.planid_path + FROM #working_metrics AS wm + JOIN patterns + ON wm.query_id = patterns.query_id + OPTION (RECOMPILE); +' +EXEC sys.sp_executesql @stmt = @sql_select; /* This gathers data for the #working_plan_text table @@ -1857,7 +2634,7 @@ RAISERROR(N'Gathering working plans', 0, 1) WITH NOWAIT; SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' SELECT ' + QUOTENAME(@DatabaseName, '''') + N' AS database_name, wp.plan_id, wp.query_id, - qsp.plan_group_id, qsp.engine_version, qsp.compatibility_level, qsp.query_plan_hash, TRY_CONVERT(XML, qsp.query_plan), qsp.is_online_index_plan, qsp.is_trivial_plan, + qsp.plan_group_id, qsp.engine_version, qsp.compatibility_level, qsp.query_plan_hash, TRY_CAST(qsp.query_plan AS XML), qsp.is_online_index_plan, qsp.is_trivial_plan, qsp.is_parallel_plan, qsp.is_forced_plan, qsp.is_natively_compiled, qsp.force_failure_count, qsp.last_force_failure_reason_desc, qsp.count_compiles, qsp.initial_compile_start_time, qsp.last_compile_start_time, qsp.last_execution_time, (qsp.avg_compile_duration / 1000.), @@ -1905,29 +2682,6 @@ EXEC sys.sp_executesql @stmt = @sql_select, -/* - -Some memory grant information isn't available in query store - -We have to go back to other DMVs to find it, when possible - -It may not be there for various reaons - -*/ -RAISERROR(N'Checking dm_exec_query_stats for memory grant info', 0, 1) WITH NOWAIT; -WITH max_mem -AS ( SELECT deqs.sql_handle, MAX(deqs.min_grant_kb) AS min_grant_kb, MAX(deqs.max_used_grant_kb) AS max_used_grant_kb - FROM sys.dm_exec_query_stats AS deqs - GROUP BY deqs.sql_handle ) -UPDATE wpt -SET wpt.min_grant_kb = deqs.min_grant_kb, - wpt.max_used_grant_kb = deqs.max_used_grant_kb -FROM #working_plan_text AS wpt -JOIN max_mem AS deqs -ON wpt.statement_sql_handle = deqs.sql_handle -OPTION (RECOMPILE); - - /* This gets us context settings for our queries and adds it to the #working_plan_text table */ @@ -2040,7 +2794,7 @@ IF @waitstats = 1 FROM #working_plan_text AS wpt JOIN ( SELECT wws.plan_id, - top_three_waits = STUFF((SELECT TOP 3 N', ' + wws2.wait_category_desc + N' (' + CONVERT(NVARCHAR(20), SUM(CONVERT(BIGINT, wws2.avg_query_wait_time_ms))) + N' ms) ' + top_three_waits = STUFF((SELECT TOP (3) N', ' + wws2.wait_category_desc + N' (' + CONVERT(NVARCHAR(20), SUM(CONVERT(BIGINT, wws2.avg_query_wait_time_ms))) + N' ms) ' FROM #working_wait_stats AS wws2 WHERE wws.plan_id = wws2.plan_id GROUP BY wws2.wait_category_desc @@ -2058,10 +2812,12 @@ END; UPDATE #working_plan_text SET top_three_waits = CASE - WHEN @waitstats = 0 THEN N'The query store waits stats DMV is not available' + WHEN @waitstats = 0 + THEN N'The query store waits stats DMV is not available' ELSE N'No Significant waits detected!' END -WHERE top_three_waits IS NULL; +WHERE top_three_waits IS NULL +OPTION(RECOMPILE); END; END TRY @@ -2097,7 +2853,7 @@ RAISERROR(N'Populate working warnings table with gathered plans', 0, 1) WITH NOW SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; SET @sql_select += N' -SELECT wp.plan_id, wp.query_id, qsq.query_hash, qsqt.statement_sql_handle +SELECT DISTINCT wp.plan_id, wp.query_id, qsq.query_hash, qsqt.statement_sql_handle FROM #working_plans AS wp JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp ON qsp.plan_id = wp.plan_id @@ -2178,12 +2934,14 @@ WHERE 1 = 1 SET @sql_select += @sql_where; -SET @sql_select += N'GROUP BY wp.query_id - HAVING COUNT(qsp.plan_id) > 1 - ) AS x - ON ww.query_id = x.query_id - OPTION (RECOMPILE); - '; +SET @sql_select += +N'GROUP BY wp.query_id + HAVING COUNT(qsp.plan_id) > 1 +) AS x + ON ww.query_id = x.query_id +OPTION (RECOMPILE); +'; + IF @Debug = 1 PRINT @sql_select; @@ -2251,7 +3009,6 @@ This looks for cursors */ RAISERROR(N'Checking for cursors', 0, 1) WITH NOWAIT; - UPDATE ww SET ww.is_cursor = 1 FROM #working_warnings AS ww @@ -2262,9 +3019,22 @@ ON ww.plan_id = wp.plan_id OPTION (RECOMPILE); +UPDATE ww +SET ww.is_cursor = 1 +FROM #working_warnings AS ww +JOIN #working_plan_text AS wp +ON ww.plan_id = wp.plan_id + AND ww.query_id = wp.query_id +WHERE ww.query_hash = 0x0000000000000000 +OR wp.query_plan_hash = 0x0000000000000000 +OPTION (RECOMPILE); + /* This looks for parallel plans */ + +RAISERROR(N'Checking for parallel plans', 0, 1) WITH NOWAIT; + UPDATE ww SET ww.is_parallel = 1 FROM #working_warnings AS ww @@ -2290,19 +3060,6 @@ OPTION (RECOMPILE); /*NO SERIOUSLY THIS IS A HORRIBLE IDEA*/ -/*This looks for trivial plans*/ - -RAISERROR(N'Checking for trivial plans', 0, 1) WITH NOWAIT; - -UPDATE w -SET w.is_trivial = 1 -FROM #working_warnings AS w -JOIN #working_plan_text AS wpt -ON w.plan_id = wpt.plan_id -AND w.query_id = wpt.query_id -AND wpt.is_trivial_plan = 1 -OPTION (RECOMPILE); - /*Plans that compile 2x more than they execute*/ RAISERROR(N'Checking for plans that compile 2x more than they execute', 0, 1) WITH NOWAIT; @@ -2345,8 +3102,8 @@ RAISERROR(N'Begin XML nodes parsing', 0, 1) WITH NOWAIT; RAISERROR(N'Inserting #statements', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement +INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement, is_cursor ) + SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement, 0 AS is_cursor FROM #working_warnings AS ww JOIN #working_plan_text AS wp ON ww.plan_id = wp.plan_id @@ -2356,8 +3113,8 @@ OPTION (RECOMPILE); RAISERROR(N'Inserting parsed cursor XML to #statements', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement +INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement, is_cursor ) + SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement, 1 AS is_cursor FROM #working_warnings AS ww JOIN #working_plan_text AS wp ON ww.plan_id = wp.plan_id @@ -2405,7 +3162,8 @@ ON s.query_hash = b.query_hash WHERE s.statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 OPTION (RECOMPILE); - +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), index_dml AS ( @@ -2440,7 +3198,28 @@ table_dml AS ( ON t.query_hash = b.query_hash WHERE t.table_dml = 1 OPTION (RECOMPILE); +END; + + +RAISERROR(N'Gathering trivial plans', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +UPDATE b +SET b.is_trivial = 1 +FROM #working_warnings AS b +JOIN ( +SELECT s.sql_handle +FROM #statements AS s +JOIN ( SELECT r.sql_handle + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[contains(@LogicalOp, "Scan")]') = 1 ) AS r + ON r.sql_handle = s.sql_handle +WHERE s.statement.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 +) AS s +ON b.sql_handle = s.sql_handle +OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #est_rows (query_hash, estimated_rows) @@ -2457,6 +3236,7 @@ WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; JOIN #est_rows er ON er.query_hash = b.query_hash OPTION (RECOMPILE); +END; /*Begin plan cost calculations*/ @@ -2513,7 +3293,39 @@ ON qp.sql_handle = b.sql_handle AND qp.query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END +FROM #working_warnings p + JOIN ( + SELECT qs.sql_handle, + relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , + relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions + FROM #relop qs + ) AS x ON p.sql_handle = x.sql_handle +OPTION (RECOMPILE); +END; + +RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END +FROM #working_warnings p + JOIN ( + SELECT r.sql_handle, + 1 AS tvf_join + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 + AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 + ) AS x ON p.sql_handle = x.sql_handle +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) , x AS ( @@ -2531,6 +3343,7 @@ SET b.warning_no_join_predicate = x.warning_no_join_predicate, FROM #working_warnings b JOIN x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); +END; RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; @@ -2542,16 +3355,19 @@ FROM #relop r CROSS APPLY r.relop.nodes('//p:Object') AS c(n) ) UPDATE b -SET b.is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END +SET b.is_table_variable = 1 FROM #working_warnings b JOIN x ON x.sql_handle = b.sql_handle JOIN #working_metrics AS wm ON b.plan_id = wm.plan_id AND b.query_id = wm.query_id AND wm.batch_sql_handle IS NOT NULL +WHERE x.first_char = '@' OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) , x AS ( @@ -2567,6 +3383,7 @@ SET b.function_count = x.function_count, FROM #working_warnings b JOIN x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); +END; RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; @@ -2577,9 +3394,10 @@ FROM #working_warnings b JOIN ( SELECT r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost + MAX(r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS key_lookup_cost FROM #relop r WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 +GROUP BY r.sql_handle ) AS x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); @@ -2592,9 +3410,10 @@ FROM #working_warnings b JOIN ( SELECT r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost + MAX(r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS remote_query_cost FROM #relop r WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 +GROUP BY r.sql_handle ) AS x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); @@ -2602,32 +3421,90 @@ OPTION (RECOMPILE); RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b -SET b.sort_cost = (x.sort_io + x.sort_cpu) +SET sort_cost = y.max_sort_cost FROM #working_warnings b JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - r.relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop r -WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle + SELECT x.sql_handle, MAX((x.sort_io + x.sort_cpu)) AS max_sort_cost + FROM ( + SELECT + qs.sql_handle, + relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, + relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu + FROM #relop qs + WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 + ) AS x + GROUP BY x.sql_handle + ) AS y +ON b.sql_handle = y.sql_handle +OPTION (RECOMPILE); + +IF NOT EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'No cursor plans found, skipping', 0, 1) WITH NOWAIT; + +END + +IF EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'Cursor plans found, investigating', 0, 1) WITH NOWAIT; + +RAISERROR(N'Checking for Optimistic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_optimistic_cursor = 1 +FROM #working_warnings b +JOIN #statements AS s +ON b.sql_handle = s.sql_handle +CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 +AND s.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking if cursor is Forward Only', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_forward_only_cursor = 1 +FROM #working_warnings b +JOIN #statements AS s +ON b.sql_handle = s.sql_handle +CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 +AND s.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking if cursor is Fast Forward', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_fast_forward_cursor = 1 +FROM #working_warnings b +JOIN #statements AS s +ON b.sql_handle = s.sql_handle +CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE n1.fn.exist('//p:CursorPlan/@CursorActualType[.="FastForward"]') = 1 +AND s.is_cursor = 1 OPTION (RECOMPILE); -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; +RAISERROR(N'Checking for Dynamic cursors', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END +SET b.is_cursor_dynamic = 1 FROM #working_warnings b JOIN #statements AS s ON b.sql_handle = s.sql_handle -AND b.is_cursor = 1 -CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n(fn) +CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE n1.fn.exist('//p:CursorPlan/@CursorActualType[.="Dynamic"]') = 1 +AND s.is_cursor = 1 OPTION (RECOMPILE); +END +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; ;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2660,8 +3537,11 @@ FROM #relop r CROSS APPLY r.relop.nodes('//p:TableScan') AS q(n) ) AS x ON b.sql_handle = x.sql_handle OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2675,6 +3555,7 @@ CROSS APPLY r.relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedVal WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 ) AS x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); +END; RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; @@ -2692,6 +3573,8 @@ CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Comp OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), IndexOps AS @@ -2743,8 +3626,11 @@ SET b.index_insert_count = iops.index_insert_count, FROM #working_warnings AS b JOIN iops ON iops.query_hash = b.query_hash OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2758,7 +3644,7 @@ CROSS APPLY r.relop.nodes('/p:RelOp//p:Object') n(fn) WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 ) AS x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); - +END; RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) @@ -2772,6 +3658,8 @@ AND qp.query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2785,8 +3673,11 @@ FROM #relop r WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 ) AS x ON x.sql_handle = b.sql_handle OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR('Checking for row level security only', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2796,7 +3687,11 @@ JOIN #statements s ON s.query_hash = b.query_hash WHERE s.statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 OPTION (RECOMPILE); +END; + +IF @ExpertMode > 0 +BEGIN RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -2810,7 +3705,7 @@ AS ( SELECT DISTINCT r.plan_id, c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds FROM #relop AS r JOIN selects AS s ON s.plan_id = r.plan_id @@ -2820,32 +3715,38 @@ WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager S ) UPDATE ww SET ww.index_spool_rows = sp.estimated_rows, - ww.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) + ww.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) + FROM #working_warnings ww JOIN spools sp ON ww.plan_id = sp.plan_id AND ww.query_id = sp.query_id -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); +END; IF (PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) >= 14 +OR ((PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) = 13 + AND PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 2) >= 5026) BEGIN -RAISERROR(N'Beginning 2017 specfic checks', 0, 1) WITH NOWAIT; +RAISERROR(N'Beginning 2017 and 2016 SP2 specific checks', 0, 1) WITH NOWAIT; +IF @ExpertMode > 0 +BEGIN RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) INSERT #stats_agg WITH (TABLOCK) (sql_handle, last_update, modification_count, sampling_percent, [statistics], [table], [schema], [database]) SELECT qp.sql_handle, x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, + x.c.value('@ModificationCount', 'BIGINT') AS ModificationCount, x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] + x.c.value('@Statistics', 'NVARCHAR(258)') AS [Statistics], + x.c.value('@Table', 'NVARCHAR(258)') AS [Table], + x.c.value('@Schema', 'NVARCHAR(258)') AS [Schema], + x.c.value('@Database', 'NVARCHAR(258)') AS [Database] FROM #query_plan AS qp CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) OPTION (RECOMPILE); @@ -2865,8 +3766,12 @@ FROM #working_warnings AS b JOIN stale_stats os ON b.sql_handle = os.sql_handle OPTION (RECOMPILE); +END; +IF (PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) >= 14 + AND @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for Adaptive Joins', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), aj AS ( @@ -2881,6 +3786,25 @@ FROM #working_warnings AS b JOIN aj ON b.sql_handle = aj.sql_handle OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN; +RAISERROR(N'Checking for Row Goals', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +row_goals AS( +SELECT qs.query_hash +FROM #relop qs +WHERE relop.value('sum(/p:RelOp/@EstimateRowsWithoutRowGoal)', 'float') > 0 +) +UPDATE b +SET b.is_row_goal = 1 +FROM #working_warnings b +JOIN row_goals +ON b.query_hash = row_goals.query_hash +OPTION (RECOMPILE); +END; END; @@ -2889,7 +3813,7 @@ RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b SET b.missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - b.unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') + b.unmatched_index_count = CASE WHEN is_trivial <> 1 THEN query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') END FROM #query_plan qp JOIN #working_warnings AS b ON b.query_hash = qp.query_hash @@ -2932,6 +3856,33 @@ JOIN #trace_flags tf ON tf.sql_handle = b.sql_handle OPTION (RECOMPILE); + +RAISERROR(N'Checking for MSTVFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mstvf = 1 +FROM #relop AS r +JOIN #working_warnings AS b +ON b.sql_handle = r.sql_handle +WHERE r.relop.exist('/p:RelOp[(@EstimateRows="100" or @EstimateRows="1") and @LogicalOp="Table-valued function"]') = 1 +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for many to many merge joins', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mm_join = 1 +FROM #relop AS r +JOIN #working_warnings AS b +ON b.sql_handle = r.sql_handle +WHERE r.relop.exist('/p:RelOp/p:Merge/@ManyToMany[.="1"]') = 1 +OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), is_paul_white_electric AS ( @@ -2947,12 +3898,48 @@ FROM #working_warnings AS b JOIN is_paul_white_electric ipwe ON ipwe.sql_handle = b.sql_handle OPTION (RECOMPILE); +END; + + + +RAISERROR(N'Checking for non-sargable predicates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, nsarg + AS ( SELECT r.query_hash, 1 AS fn, 0 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator') AS ca(x) + WHERE ( ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName') = 1 + OR ca.x.exist('//p:ScalarOperator/p:IF') = 1 ) + UNION ALL + SELECT r.query_hash, 0 AS fn, 1 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp//p:ScalarOperator') AS ca(x) + WHERE r.relop.exist('/p:RelOp[contains(@LogicalOp, "Join")]') = 1 + AND ca.x.exist('//p:ScalarOperator[contains(@ScalarString, "Expr")]') = 1 + UNION ALL + SELECT r.query_hash, 0 AS fn, 0 AS jo, 1 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator') AS ca(x) + CROSS APPLY ca.x.nodes('//p:Const') AS co(x) + WHERE ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName[.="like"]') = 1 + AND ( ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') <> 'N' + AND co.x.value('substring(@ConstValue, 2, 1)', 'VARCHAR(100)') = '%' ) + OR ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') = 'N' + AND co.x.value('substring(@ConstValue, 3, 1)', 'VARCHAR(100)') = '%' ))), + d_nsarg + AS ( SELECT DISTINCT + nsarg.query_hash + FROM nsarg + WHERE nsarg.fn = 1 + OR nsarg.jo = 1 + OR nsarg.lk = 1 ) +UPDATE b +SET b.is_nonsargable = 1 +FROM d_nsarg AS d +JOIN #working_warnings AS b + ON b.query_hash = d.query_hash +OPTION ( RECOMPILE ); -IF EXISTS ( SELECT 1 - FROM #working_warnings AS ww - WHERE ww.implicit_conversions = 1 - OR ww.proc_or_function_name <> N'Statement' ) - BEGIN RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; @@ -2963,15 +3950,15 @@ IF EXISTS ( SELECT 1 qp.query_hash, qp.sql_handle, b.proc_or_function_name AS proc_name, - q.n.value('@Column', 'NVARCHAR(256)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(256)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(4000)') AS compile_time_value + q.n.value('@Column', 'NVARCHAR(258)') AS variable_name, + q.n.value('@ParameterDataType', 'NVARCHAR(258)') AS variable_datatype, + q.n.value('@ParameterCompiledValue', 'NVARCHAR(258)') AS compile_time_value FROM #query_plan AS qp JOIN #working_warnings AS b ON (b.query_hash = qp.query_hash AND b.proc_or_function_name = 'adhoc') OR (b.sql_handle = qp.sql_handle AND b.proc_or_function_name <> 'adhoc') CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -2988,7 +3975,7 @@ IF EXISTS ( SELECT 1 CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 AND b.implicit_conversions = 1 - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; INSERT #stored_proc_info ( sql_handle, query_hash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) @@ -3012,7 +3999,7 @@ IF EXISTS ( SELECT 1 AND ci.convert_implicit_charindex = 0 THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 + AND (ci.equal_charindex -1) > 0 AND ci.convert_implicit_charindex > 0 THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) WHEN ci.at_charindex > 0 @@ -3032,7 +4019,7 @@ IF EXISTS ( SELECT 1 ELSE '**idk_man**' END AS compile_time_value FROM #conversion_info AS ci - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); RAISERROR(N'Updating variables inserted procs', 0, 1) WITH NOWAIT; UPDATE sp @@ -3043,7 +4030,7 @@ IF EXISTS ( SELECT 1 ON (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); RAISERROR(N'Inserting variables for other procs', 0, 1) WITH NOWAIT; @@ -3058,42 +4045,72 @@ IF EXISTS ( SELECT 1 WHERE (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) ) - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; UPDATE s - SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) + SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' + THEN LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) ELSE s.variable_datatype END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) + s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' + THEN LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) ELSE s.converted_to END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) + s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' + THEN SUBSTRING(s.compile_time_value, + CHARINDEX('(', s.compile_time_value) + 1, + CHARINDEX(')', s.compile_time_value) - 1 - CHARINDEX('(', s.compile_time_value) + ) WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') AND s.variable_datatype NOT LIKE '%binary%' AND s.compile_time_value NOT LIKE 'N''%''' - AND s.compile_time_value NOT LIKE '''%''' THEN - QUOTENAME(compile_time_value, '''') + AND s.compile_time_value NOT LIKE '''%''' + AND s.compile_time_value <> s.column_name + AND s.compile_time_value <> '**idk_man**' + THEN QUOTENAME(compile_time_value, '''') ELSE s.compile_time_value END FROM #stored_proc_info AS s + OPTION (RECOMPILE); + + + RAISERROR(N'Updating SET options', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE s + SET set_options = set_options.ansi_set_options + FROM #stored_proc_info AS s + JOIN ( + SELECT x.sql_handle, + N'SET ANSI_NULLS ' + CASE WHEN [ANSI_NULLS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_PADDING ' + CASE WHEN [ANSI_PADDING] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_WARNINGS ' + CASE WHEN [ANSI_WARNINGS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ARITHABORT ' + CASE WHEN [ARITHABORT] = 'true' THEN N'ON ' ELSE N' OFF ' END + NCHAR(10) + + N'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN [CONCAT_NULL_YIELDS_NULL] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET NUMERIC_ROUNDABORT ' + CASE WHEN [NUMERIC_ROUNDABORT] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET QUOTED_IDENTIFIER ' + CASE WHEN [QUOTED_IDENTIFIER] = 'true' THEN N'ON ' ELSE N'OFF ' + NCHAR(10) END AS [ansi_set_options] + FROM ( + SELECT + s.sql_handle, + so.o.value('@ANSI_NULLS', 'NVARCHAR(20)') AS [ANSI_NULLS], + so.o.value('@ANSI_PADDING', 'NVARCHAR(20)') AS [ANSI_PADDING], + so.o.value('@ANSI_WARNINGS', 'NVARCHAR(20)') AS [ANSI_WARNINGS], + so.o.value('@ARITHABORT', 'NVARCHAR(20)') AS [ARITHABORT], + so.o.value('@CONCAT_NULL_YIELDS_NULL', 'NVARCHAR(20)') AS [CONCAT_NULL_YIELDS_NULL], + so.o.value('@NUMERIC_ROUNDABORT', 'NVARCHAR(20)') AS [NUMERIC_ROUNDABORT], + so.o.value('@QUOTED_IDENTIFIER', 'NVARCHAR(20)') AS [QUOTED_IDENTIFIER] + FROM #statements AS s + CROSS APPLY s.statement.nodes('//p:StatementSetOptions') AS so(o) + ) AS x + ) AS set_options ON set_options.sql_handle = s.sql_handle OPTION(RECOMPILE); + RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; WITH precheck AS ( SELECT spi.sql_handle, spi.proc_name, - CONVERT(XML, - N' 'Statement' + (SELECT CASE WHEN spi.proc_name <> 'Statement' THEN N'The stored procedure ' + spi.proc_name ELSE N'This ad hoc statement' END @@ -3137,6 +4154,7 @@ IF EXISTS ( SELECT 1 WHEN spi2.column_name LIKE '%Expr%' THEN N'' WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') + AND spi2.compile_time_value <> spi2.column_name THEN ' with the value ' + RTRIM(spi2.compile_time_value) ELSE N'' END @@ -3144,9 +4162,8 @@ IF EXISTS ( SELECT 1 FROM #stored_proc_info AS spi2 WHERE spi.sql_handle = spi2.sql_handle FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS implicit_conversion_info FROM #stored_proc_info AS spi GROUP BY spi.sql_handle, spi.proc_name ) @@ -3155,14 +4172,14 @@ IF EXISTS ( SELECT 1 FROM #working_warnings AS b JOIN precheck AS pk ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); + OPTION (RECOMPILE); - RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; + RAISERROR(N'Updating cached parameter XML for procs', 0, 1) WITH NOWAIT; WITH precheck AS ( SELECT spi.sql_handle, spi.proc_name, - CONVERT(XML, - N' N'Statement' FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @cr + @lf - + N' -- ?>' - ) AS cached_execution_parameters + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters FROM #stored_proc_info AS spi - GROUP BY spi.sql_handle, spi.proc_name + GROUP BY spi.sql_handle, spi.proc_name, set_options ) UPDATE b SET b.cached_execution_parameters = pk.cached_execution_parameters FROM #working_warnings AS b JOIN precheck AS pk ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); + WHERE b.proc_or_function_name <> N'Statement' + OPTION (RECOMPILE); - END; --End implicit conversion information gathering + RAISERROR(N'Updating cached parameter XML for statements', 0, 1) WITH NOWAIT; + WITH precheck AS ( + SELECT spi.sql_handle, + spi.proc_name, + (SELECT + set_options + + @cr + @lf + + @cr + @lf + + N' See QueryText column for full query text' + + @cr + @lf + + @cr + @lf + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE + @cr + @lf + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN + @cr + @lf + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.sql_handle = spi2.sql_handle + AND spi2.proc_name = N'Statement' + AND spi2.variable_name NOT LIKE N'%msparam%' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters + FROM #stored_proc_info AS spi + GROUP BY spi.sql_handle, spi.proc_name, spi.set_options + ) + UPDATE b + SET b.cached_execution_parameters = pk.cached_execution_parameters + FROM #working_warnings AS b + JOIN precheck AS pk + ON pk.sql_handle = b.sql_handle + WHERE b.proc_or_function_name = N'Statement' + OPTION (RECOMPILE); +RAISERROR(N'Filling in implicit conversion info', 0, 1) WITH NOWAIT; UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN - N'' - ELSE b.implicit_conversion_info +SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL + OR CONVERT(NVARCHAR(MAX), b.implicit_conversion_info) = N'' + THEN N'' + ELSE b.implicit_conversion_info END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN - N'' - ELSE b.cached_execution_parameters + b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL + OR CONVERT(NVARCHAR(MAX), b.cached_execution_parameters) = N'' + THEN N'' + ELSE b.cached_execution_parameters END FROM #working_warnings AS b -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); -/*Begin Missing Index*/ +/*End implicit conversion and parameter info*/ -IF EXISTS - (SELECT 1 FROM #working_warnings AS ww WHERE ww.missing_index_count > 0 ) - - BEGIN +/*Begin Missing Index*/ +IF EXISTS ( SELECT 1/0 + FROM #working_warnings AS ww + WHERE ww.missing_index_count > 0 + OR ww.index_spool_cost > 0 + OR ww.index_spool_rows > 0 ) + + BEGIN RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -3230,19 +4293,19 @@ IF EXISTS CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) WHERE qp.query_hash IS NOT NULL AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); + OPTION (RECOMPILE); RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #missing_index_schema SELECT mix.query_hash, mix.sql_handle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , + c.mi.value('@Database', 'NVARCHAR(128)'), + c.mi.value('@Schema', 'NVARCHAR(128)'), + c.mi.value('@Table', 'NVARCHAR(128)'), c.mi.query('.') FROM #missing_index_xml AS mix CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); + OPTION (RECOMPILE); RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -3252,7 +4315,7 @@ IF EXISTS c.cg.query('.') FROM #missing_index_schema ms CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); + OPTION (RECOMPILE); RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -3267,11 +4330,11 @@ IF EXISTS c.c.value('@Name', 'NVARCHAR(128)') FROM #missing_index_usage AS miu CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); + OPTION (RECOMPILE); RAISERROR(N'Inserting to #missing_index_pretty', 0, 1) WITH NOWAIT; INSERT #missing_index_pretty - SELECT m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name + SELECT DISTINCT m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'EQUALITY' @@ -3281,7 +4344,7 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'INEQUALITY' @@ -3291,7 +4354,7 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'INCLUDE' @@ -3301,17 +4364,85 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS [include], + 0 AS is_spool FROM #missing_index_detail AS m GROUP BY m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); + OPTION (RECOMPILE); + + RAISERROR(N'Inserting to #index_spool_ugly', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + INSERT #index_spool_ugly + (query_hash, sql_handle, impact, database_name, schema_name, table_name, equality, inequality, include) + SELECT r.query_hash, + r.sql_handle, + (c.n.value('@EstimateIO', 'FLOAT') + (c.n.value('@EstimateCPU', 'FLOAT'))) + / ( 1 * NULLIF(ww.query_cost, 0)) * 100 AS impact, + o.n.value('@Database', 'NVARCHAR(128)') AS output_database, + o.n.value('@Schema', 'NVARCHAR(128)') AS output_schema, + o.n.value('@Table', 'NVARCHAR(128)') AS output_table, + k.n.value('@Column', 'NVARCHAR(128)') AS range_column, + e.n.value('@Column', 'NVARCHAR(128)') AS expression_column, + o.n.value('@Column', 'NVARCHAR(128)') AS output_column + FROM #relop AS r + JOIN #working_warnings AS ww + ON ww.query_hash = r.query_hash + CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) + CROSS APPLY r.relop.nodes('/p:RelOp/p:OutputList/p:ColumnReference') AS o(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeColumns/p:ColumnReference') AS k(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeExpressions/p:ColumnReference') AS e(n) + WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 + RAISERROR(N'Inserting to spools to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + (query_hash, sql_handle, impact, database_name, schema_name, table_name, equality, inequality, include, is_spool) + SELECT DISTINCT + isu.query_hash, + isu.sql_handle, + isu.impact, + isu.database_name, + isu.schema_name, + isu.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.equality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.equality IS NOT NULL + AND isu.query_hash = isu2.query_hash + AND isu.sql_handle = isu2.sql_handle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.inequality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.inequality IS NOT NULL + AND isu.query_hash = isu2.query_hash + AND isu.sql_handle = isu2.sql_handle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.include, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.include IS NOT NULL + AND isu.query_hash = isu2.query_hash + AND isu.sql_handle = isu2.sql_handle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS include, + 1 AS is_spool + FROM #index_spool_ugly AS isu + + RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; WITH missing AS ( - SELECT mip.query_hash, + SELECT DISTINCT + mip.query_hash, mip.sql_handle, - CONVERT(XML, - N'' - ) AS full_details + + N']]>' + AS full_details FROM #missing_index_pretty AS mip GROUP BY mip.query_hash, mip.sql_handle, mip.impact ) @@ -3331,10 +4462,7 @@ IF EXISTS FROM #working_warnings AS ww JOIN missing AS m ON m.sql_handle = ww.sql_handle - OPTION(RECOMPILE); - - - END + OPTION (RECOMPILE); RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; UPDATE ww @@ -3344,8 +4472,9 @@ IF EXISTS ELSE ww.missing_indexes END FROM #working_warnings AS ww - OPTION(RECOMPILE); + OPTION (RECOMPILE); +END /*End Missing Index*/ RAISERROR(N'General query dispositions: frequent executions, long running, etc.', 0, 1) WITH NOWAIT; @@ -3358,12 +4487,12 @@ SET b.frequent_execution = CASE WHEN wm.xpm > @execution_threshold THEN 1 END WHEN wm.max_duration > @long_running_query_warning_seconds THEN 1 WHEN wm.avg_cpu_time > @long_running_query_warning_seconds THEN 1 WHEN wm.max_cpu_time > @long_running_query_warning_seconds THEN 1 END, - b.is_key_lookup_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.key_lookup_cost >= b.query_cost * .5 THEN 1 END, - b.is_sort_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.sort_cost >= b.query_cost * .5 THEN 1 END, + b.is_key_lookup_expensive = CASE WHEN b.query_cost >= (@ctp / 2) AND b.key_lookup_cost >= b.query_cost * .5 THEN 1 END, + b.is_sort_expensive = CASE WHEN b.query_cost >= (@ctp / 2) AND b.sort_cost >= b.query_cost * .5 THEN 1 END, b.is_remote_query_expensive = CASE WHEN b.remote_query_cost >= b.query_cost * .05 THEN 1 END, - b.is_unused_grant = CASE WHEN percent_memory_grant_used <= @memory_grant_warning_percent AND min_grant_kb > @min_memory_per_query THEN 1 END, - b.long_running_low_cpu = CASE WHEN wm.avg_duration > wm.avg_cpu_time * 4 THEN 1 END, - b.low_cost_high_cpu = CASE WHEN b.query_cost < @ctp AND wm.avg_cpu_time > 500. AND b.query_cost * 10 < wm.avg_cpu_time THEN 1 END, + b.is_unused_grant = CASE WHEN percent_memory_grant_used <= @memory_grant_warning_percent AND min_query_max_used_memory > @min_memory_per_query THEN 1 END, + b.long_running_low_cpu = CASE WHEN wm.avg_duration > wm.avg_cpu_time * 4 AND avg_cpu_time < 500. THEN 1 END, + b.low_cost_high_cpu = CASE WHEN b.query_cost < 10 AND wm.avg_cpu_time > 5000. THEN 1 END, b.is_spool_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.index_spool_cost >= b.query_cost * .1 THEN 1 END, b.is_spool_more_rows = CASE WHEN b.index_spool_rows >= wm.min_rowcount THEN 1 END, b.is_bad_estimate = CASE WHEN wm.avg_rowcount > 0 AND (b.estimated_rows * 1000 < wm.avg_rowcount OR b.estimated_rows > wm.avg_rowcount * 1000) THEN 1 END, @@ -3392,8 +4521,10 @@ SET b.warnings = SUBSTRING( CASE WHEN b.missing_index_count > 0 THEN ', Missing Indexes (' + CAST(b.missing_index_count AS NVARCHAR(3)) + ')' ELSE '' END + CASE WHEN b.unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(b.unmatched_index_count AS NVARCHAR(3)) + ')' ELSE '' END + CASE WHEN b.is_cursor = 1 THEN ', Cursor' - + CASE WHEN b.is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN b.is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END + + CASE WHEN b.is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN b.is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN b.is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN b.is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END ELSE '' END + CASE WHEN b.is_parallel = 1 THEN ', Parallel' ELSE '' END + CASE WHEN b.near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + @@ -3438,7 +4569,11 @@ SET b.warnings = SUBSTRING( CASE WHEN b.is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + CASE WHEN b.is_big_log = 1 THEN + ', High log use' ELSE '' END + CASE WHEN b.is_big_tempdb = 1 THEN ', High tempdb use' ELSE '' END + - CASE WHEN b.is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + CASE WHEN b.is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN b.is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN b.is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN b.is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN b.is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END , 2, 200000) FROM #working_warnings b OPTION (RECOMPILE); @@ -3473,6 +4608,8 @@ RAISERROR(N'Checking for parameter sniffing symptoms', 0, 1) WITH NOWAIT; UPDATE b SET b.parameter_sniffing_symptoms = +CASE WHEN b.count_executions < 2 THEN 'Too few executions to compare (< 2).' + ELSE SUBSTRING( /*Duration*/ CASE WHEN (b.min_duration * 100) < (b.avg_duration) THEN ', Fast sometimes' ELSE '' END + @@ -3510,10 +4647,9 @@ SET b.parameter_sniffing_symptoms = CASE WHEN b.last_rowcount * 100 < b.avg_rowcount THEN ', Low row count run' ELSE '' END + CASE WHEN b.last_rowcount > b.avg_rowcount * 100 THEN ', High row count last run' ELSE '' END + /*DOP*/ - CASE WHEN b.min_dop = 1 THEN ', Serial sometimes' ELSE '' END + - CASE WHEN b.max_dop > 1 THEN ', Parallel sometimes' ELSE '' END + - CASE WHEN b.last_dop = 1 THEN ', Serial last run' ELSE '' END + - CASE WHEN b.last_dop > 1 THEN ', Parallel last run' ELSE '' END + + CASE WHEN b.min_dop <> b.max_dop THEN ', Serial sometimes' ELSE '' END + + CASE WHEN b.min_dop <> b.max_dop AND b.last_dop = 1 THEN ', Serial last run' ELSE '' END + + CASE WHEN b.min_dop <> b.max_dop AND b.last_dop > 1 THEN ', Parallel last run' ELSE '' END + /*tempdb*/ CASE WHEN b.min_tempdb_space_used * 100 < b.avg_tempdb_space_used THEN ', Low tempdb sometimes' ELSE '' END + CASE WHEN b.max_tempdb_space_used > b.avg_tempdb_space_used * 100 THEN ', High tempdb sometimes' ELSE '' END + @@ -3525,6 +4661,7 @@ SET b.parameter_sniffing_symptoms = CASE WHEN b.last_log_bytes_used * 100 < b.avg_log_bytes_used THEN ', Low log use run' ELSE '' END + CASE WHEN b.last_log_bytes_used > b.avg_log_bytes_used * 100 THEN ', High log use last run' ELSE '' END , 2, 200000) + END FROM #working_metrics AS b OPTION (RECOMPILE); @@ -3558,9 +4695,8 @@ BEGIN RAISERROR(N'Returning regular results', 0, 1) WITH NOWAIT; - WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, +SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wm.query_id_all_plan_ids, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, wm.parameter_sniffing_symptoms, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, @@ -3575,7 +4711,13 @@ JOIN #working_metrics AS wm ON wpt.plan_id = wm.plan_id AND wpt.query_id = wm.query_id ) -SELECT * +SELECT x.database_name, x.query_cost, x.plan_id, x.query_id, x.query_id_all_plan_ids, x.query_sql_text, x.proc_or_function_name, x.query_plan_xml, x.warnings, x.pattern, + x.parameter_sniffing_symptoms, x.top_three_waits, x.missing_indexes, x.implicit_conversion_info, x.cached_execution_parameters, x.count_executions, x.count_compiles, x.total_cpu_time, x.avg_cpu_time, + x.total_duration, x.avg_duration, x.total_logical_io_reads, x.avg_logical_io_reads, + x.total_physical_io_reads, x.avg_physical_io_reads, x.total_logical_io_writes, x.avg_logical_io_writes, x.total_rowcount, x.avg_rowcount, + x.total_query_max_used_memory, x.avg_query_max_used_memory, x.total_tempdb_space_used, x.avg_tempdb_space_used, + x.total_log_bytes_used, x.avg_log_bytes_used, x.total_num_physical_io_reads, x.avg_num_physical_io_reads, + x.first_execution_time, x.last_execution_time, x.last_force_failure_reason_desc, x.context_settings FROM x WHERE x.rn = 1 ORDER BY x.last_execution_time @@ -3589,7 +4731,7 @@ BEGIN RAISERROR(N'Returning results for failed queries', 0, 1) WITH NOWAIT; WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, +SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wm.query_id_all_plan_ids, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, @@ -3605,7 +4747,14 @@ JOIN #working_metrics AS wm ON wpt.plan_id = wm.plan_id AND wpt.query_id = wm.query_id ) -SELECT * +SELECT x.database_name, x.query_cost, x.plan_id, x.query_id, x.query_id_all_plan_ids, x.query_sql_text, x.proc_or_function_name, x.query_plan_xml, x.warnings, x.pattern, + x.parameter_sniffing_symptoms, x.last_force_failure_reason_desc, x.top_three_waits, x.missing_indexes, x.implicit_conversion_info, x.cached_execution_parameters, + x.count_executions, x.count_compiles, x.total_cpu_time, x.avg_cpu_time, + x.total_duration, x.avg_duration, x.total_logical_io_reads, x.avg_logical_io_reads, + x.total_physical_io_reads, x.avg_physical_io_reads, x.total_logical_io_writes, x.avg_logical_io_writes, x.total_rowcount, x.avg_rowcount, + x.total_query_max_used_memory, x.avg_query_max_used_memory, x.total_tempdb_space_used, x.avg_tempdb_space_used, + x.total_log_bytes_used, x.avg_log_bytes_used, x.total_num_physical_io_reads, x.avg_num_physical_io_reads, + x.first_execution_time, x.last_execution_time, x.context_settings FROM x WHERE x.rn = 1 ORDER BY x.last_execution_time @@ -3623,7 +4772,7 @@ SET query_sql_text = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(query_sql_tex OPTION (RECOMPILE); WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, ww.warnings, wpt.pattern, +SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wm.query_id_all_plan_ids, wpt.query_sql_text, wm.proc_or_function_name, ww.warnings, wpt.pattern, wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, @@ -3638,7 +4787,13 @@ JOIN #working_metrics AS wm ON wpt.plan_id = wm.plan_id AND wpt.query_id = wm.query_id ) -SELECT * +SELECT x.database_name, x.query_cost, x.plan_id, x.query_id, x.query_id_all_plan_ids, x.query_sql_text, x.proc_or_function_name, x.warnings, x.pattern, + x.parameter_sniffing_symptoms, x.last_force_failure_reason_desc, x.top_three_waits, x.count_executions, x.count_compiles, x.total_cpu_time, x.avg_cpu_time, + x.total_duration, x.avg_duration, x.total_logical_io_reads, x.avg_logical_io_reads, + x.total_physical_io_reads, x.avg_physical_io_reads, x.total_logical_io_writes, x.avg_logical_io_writes, x.total_rowcount, x.avg_rowcount, + x.total_query_max_used_memory, x.avg_query_max_used_memory, x.total_tempdb_space_used, x.avg_tempdb_space_used, + x.total_log_bytes_used, x.avg_log_bytes_used, x.total_num_physical_io_reads, x.avg_num_physical_io_reads, + x.first_execution_time, x.last_execution_time, x.context_settings FROM x WHERE x.rn = 1 ORDER BY x.last_execution_time @@ -3652,7 +4807,7 @@ BEGIN RAISERROR(N'Returning results for skipped XML', 0, 1) WITH NOWAIT; WITH x AS ( -SELECT wpt.database_name, wm.plan_id, wm.query_id, wpt.query_sql_text, wpt.query_plan_xml, wpt.pattern, +SELECT wpt.database_name, wm.plan_id, wm.query_id, wm.query_id_all_plan_ids, wpt.query_sql_text, wpt.query_plan_xml, wpt.pattern, wm.parameter_sniffing_symptoms, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, @@ -3664,7 +4819,13 @@ JOIN #working_metrics AS wm ON wpt.plan_id = wm.plan_id AND wpt.query_id = wm.query_id ) -SELECT * +SELECT x.database_name, x.plan_id, x.query_id, x.query_id_all_plan_ids, x.query_sql_text, x.query_plan_xml, x.pattern, + x.parameter_sniffing_symptoms, x.top_three_waits, x.count_executions, x.count_compiles, x.total_cpu_time, x.avg_cpu_time, + x.total_duration, x.avg_duration, x.total_logical_io_reads, x.avg_logical_io_reads, + x.total_physical_io_reads, x.avg_physical_io_reads, x.total_logical_io_writes, x.avg_logical_io_writes, x.total_rowcount, x.avg_rowcount, + x.total_query_max_used_memory, x.avg_query_max_used_memory, x.total_tempdb_space_used, x.avg_tempdb_space_used, + x.total_log_bytes_used, x.avg_log_bytes_used, x.total_num_physical_io_reads, x.avg_num_physical_io_reads, + x.first_execution_time, x.last_execution_time, x.last_force_failure_reason_desc, x.context_settings FROM x WHERE x.rn = 1 ORDER BY x.last_execution_time @@ -3711,7 +4872,7 @@ BEGIN 100, 'Execution Pattern', 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', + 'https://www.brentozar.com/blitzcache/frequently-executed-queries/', 'Queries are being executed more than ' + CAST (@execution_threshold AS VARCHAR(5)) + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; @@ -3726,7 +4887,7 @@ BEGIN 50, 'Parameterization', 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', + 'https://www.brentozar.com/blitzcache/parameter-sniffing/', 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; /* Forced execution plans */ @@ -3740,7 +4901,7 @@ BEGIN 5, 'Parameterization', 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', + 'https://www.brentozar.com/blitzcache/forced-plans/', 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); IF EXISTS (SELECT 1/0 @@ -3753,7 +4914,7 @@ BEGIN 200, 'Cursors', 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); IF EXISTS (SELECT 1/0 @@ -3767,7 +4928,7 @@ BEGIN 200, 'Cursors', 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', 'There are optimistic cursors in the plan cache, which can harm performance.'); IF EXISTS (SELECT 1/0 @@ -3781,9 +4942,35 @@ BEGIN 200, 'Cursors', 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', 'There are non-forward only cursors in the plan cache, which can harm performance.'); + IF EXISTS (SELECT 1/0 + FROM #working_warnings + WHERE is_cursor = 1 + AND is_cursor_dynamic = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (4, + 200, + 'Cursors', + 'Dynamic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Dynamic Cursors inhibit parallelism!'); + + IF EXISTS (SELECT 1/0 + FROM #working_warnings + WHERE is_cursor = 1 + AND is_fast_forward_cursor = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (4, + 200, + 'Cursors', + 'Fast Forward Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Fast forward cursors inhibit parallelism!'); + IF EXISTS (SELECT 1/0 FROM #working_warnings WHERE is_forced_parameterized = 1 @@ -3794,7 +4981,7 @@ BEGIN 50, 'Parameterization', 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', + 'https://www.brentozar.com/blitzcache/forced-parameterization/', 'Execution plans have been compiled with forced parameterization.') ; IF EXISTS (SELECT 1/0 @@ -3807,7 +4994,7 @@ BEGIN 200, 'Execution Plans', 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', + 'https://www.brentozar.com/blitzcache/parallel-plans-detected/', 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; IF EXISTS (SELECT 1/0 @@ -3820,7 +5007,7 @@ BEGIN 200, 'Execution Plans', 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', + 'https://www.brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; IF EXISTS (SELECT 1/0 @@ -3833,7 +5020,7 @@ BEGIN 50, 'Execution Plans', 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; IF EXISTS (SELECT 1/0 @@ -3846,7 +5033,7 @@ BEGIN 50, 'Performance', 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', + 'https://www.brentozar.com/blitzcache/long-running-queries/', 'Long running queries have been found. These are queries with an average duration longer than ' + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) + ' second(s). These queries should be investigated for additional tuning options.') ; @@ -3861,7 +5048,7 @@ BEGIN 50, 'Performance', 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', + 'https://www.brentozar.com/blitzcache/missing-index-request/', 'Queries found with missing indexes.'); IF EXISTS (SELECT 1/0 @@ -3874,7 +5061,7 @@ BEGIN 200, 'Cardinality', 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', + 'https://www.brentozar.com/blitzcache/legacy-cardinality-estimator/', 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); IF EXISTS (SELECT 1/0 @@ -3887,9 +5074,35 @@ BEGIN 50, 'Performance', 'Implicit Conversions', - 'http://brentozar.com/go/implicit', + 'https://www.brentozar.com/go/implicit', 'One or more queries are comparing two fields that are not of the same data type.') ; + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE busy_loops = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES ( + 16, + 100, + 'Performance', + 'Busy Loops', + 'https://www.brentozar.com/blitzcache/busy-loops/', + 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); + + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE tvf_join = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES ( + 17, + 50, + 'Performance', + 'Joining to table valued functions', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + IF EXISTS (SELECT 1/0 FROM #working_warnings WHERE compile_timeout = 1 @@ -3900,7 +5113,7 @@ BEGIN 50, 'Execution Plans', 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', + 'https://www.brentozar.com/blitzcache/compilation-timeout/', 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); IF EXISTS (SELECT 1/0 @@ -3913,7 +5126,7 @@ BEGIN 50, 'Execution Plans', 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', + 'https://www.brentozar.com/blitzcache/compile-memory-limit-exceeded/', 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); IF EXISTS (SELECT 1/0 @@ -3926,7 +5139,7 @@ BEGIN 10, 'Execution Plans', 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', + 'https://www.brentozar.com/blitzcache/no-join-predicate/', 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); IF EXISTS (SELECT 1/0 @@ -3939,7 +5152,7 @@ BEGIN 200, 'Execution Plans', 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', + 'https://www.brentozar.com/blitzcache/multiple-plans/', 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); IF EXISTS (SELECT 1/0 @@ -3952,7 +5165,7 @@ BEGIN 100, 'Performance', 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', + 'https://www.brentozar.com/blitzcache/unmatched-indexes', 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); IF EXISTS (SELECT 1/0 @@ -3965,7 +5178,7 @@ BEGIN 100, 'Parameterization', 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', + 'https://www.brentozar.com/blitzcache/unparameterized-queries', 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); IF EXISTS (SELECT 1/0 @@ -3978,7 +5191,7 @@ BEGIN 100, 'Execution Plans', 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', + 'https://www.brentozar.com/blitzcache/trivial-plans', 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); IF EXISTS (SELECT 1/0 @@ -3991,7 +5204,7 @@ BEGIN 10, 'Execution Plans', 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', + 'https://www.brentozar.com/blitzcache/forced-serialization/', 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; IF EXISTS (SELECT 1/0 @@ -4004,7 +5217,7 @@ BEGIN 100, 'Execution Plans', 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', + 'https://www.brentozar.com/blitzcache/expensive-key-lookups/', 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 @@ -4017,7 +5230,7 @@ BEGIN 100, 'Execution Plans', 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', + 'https://www.brentozar.com/blitzcache/expensive-remote-query/', 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 @@ -4070,7 +5283,7 @@ BEGIN 'Compute Scalar That References A CLR Function', 'This could be trouble if your CLR functions perform data access', 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; + 'May force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; IF EXISTS (SELECT 1/0 @@ -4109,7 +5322,7 @@ BEGIN 100, 'Operator Warnings', 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', 'Check the plan for more details.') ; IF EXISTS (SELECT 1/0 @@ -4201,7 +5414,7 @@ BEGIN 100, 'Execution Plans', 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', + 'https://www.brentozar.com/blitzcache/expensive-sorts/', 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 @@ -4397,7 +5610,56 @@ BEGIN 'High tempdb use', 'This query uses more than half of a data file on average', 'No URL yet', - 'You should take a look at tempdb waits to see if you''re having problems') ; + 'You should take a look at tempdb waits to see if you''re having problems') ; + + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE p.is_row_goal = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (59, + 200, + 'Row Goals', + 'This query had row goals introduced', + 'https://www.brentozar.com/archive/2018/01/sql-server-2017-cu3-adds-optimizer-row-goal-information-query-plans/', + 'This can be good or bad, and should be investigated for high read queries') ; + + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE p.is_mstvf = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES ( + 60, + 100, + 'MSTVFs', + 'These have many of the same problems scalar UDFs have', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE p.is_mstvf = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (61, + 100, + 'Many to Many Merge', + 'These use secret worktables that could be doing lots of reads', + 'https://www.brentozar.com/archive/2018/04/many-mysteries-merge-joins/', + 'Occurs when join inputs aren''t known to be unique. Can be really bad when parallel.'); + + IF EXISTS (SELECT 1/0 + FROM #working_warnings p + WHERE p.is_nonsargable = 1 + ) + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (62, + 50, + 'Non-SARGable queries', + 'Queries may be using', + 'https://www.brentozar.com/blitzcache/non-sargable-predicates/', + 'Occurs when predicates cannot be used for index seeks. Causes all kinds of bad side effects.'); IF EXISTS (SELECT 1/0 FROM #working_warnings p @@ -4409,8 +5671,10 @@ BEGIN 200, 'Is Paul White Electric?', 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; + 'https://www.sql.kiwi/2013/06/hello-operator-my-switch-is-bored.html', + 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; + + INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) SELECT @@ -4437,20 +5701,8 @@ BEGIN 'Global Trace Flags Enabled', 'You have Global Trace Flags enabled on your server', 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; + 'You have the following Global Trace Flags enabled: ' + (SELECT TOP (1) tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; - IF EXISTS (SELECT 1/0 - FROM #working_plan_text AS p - WHERE p.min_grant_kb IS NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1001, - 255, - 'Plans not in cache', - 'We checked sys.dm_exec_query_stats for memory grant info', - '', - 'Plans in Query Store aren''t in other DMVs, which means we can''t get some information about them.') ; /* Return worsts @@ -4468,89 +5720,163 @@ BEGIN gi.total_rowcount, gi.total_avg_log_bytes_mb, gi.total_avg_tempdb_space, + gi.total_max_duration_ms, + gi.total_max_cpu_time_ms, + gi.total_max_logical_io_reads_mb, + gi.total_max_physical_io_reads_mb, + gi.total_max_logical_io_writes_mb, + gi.total_max_query_max_used_memory_mb, + gi.total_max_log_bytes_mb, + gi.total_max_tempdb_space, CONVERT(NVARCHAR(20), gi.flat_date) AS worst_date, - CASE WHEN DATEPART(HOUR, gi.start_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.start_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range)) + 'am ' - WHEN DATEPART(HOUR, gi.start_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range) -12) + 'pm ' + CASE WHEN DATEPART(HOUR, gi.start_range) = 0 THEN 'midnight' + WHEN DATEPART(HOUR, gi.start_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range)) + 'am' + WHEN DATEPART(HOUR, gi.start_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range) -12) + 'pm' END AS worst_start_time, - CASE WHEN DATEPART(HOUR, gi.end_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.end_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range)) + 'am ' - WHEN DATEPART(HOUR, gi.end_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range) -12) + 'pm ' + CASE WHEN DATEPART(HOUR, gi.end_range) = 0 THEN 'midnight' + WHEN DATEPART(HOUR, gi.end_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range)) + 'am' + WHEN DATEPART(HOUR, gi.end_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range) -12) + 'pm' END AS worst_end_time FROM #grouped_interval AS gi - ), + ), /*averages*/ duration_worst AS ( - SELECT TOP 1 'Your worst duration range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg duration range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_duration_ms DESC ), cpu_worst AS ( - SELECT TOP 1 'Your worst cpu range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg cpu range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_cpu_time_ms DESC ), logical_reads_worst AS ( - SELECT TOP 1 'Your worst logical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg logical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_logical_io_reads_mb DESC ), physical_reads_worst AS ( - SELECT TOP 1 'Your worst physical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg physical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_physical_io_reads_mb DESC ), logical_writes_worst AS ( - SELECT TOP 1 'Your worst logical write range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg logical write range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_logical_io_writes_mb DESC ), memory_worst AS ( - SELECT TOP 1 'Your worst memory range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg memory range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_query_max_used_memory_mb DESC ), rowcount_worst AS ( - SELECT TOP 1 'Your worst row count range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg row count range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_rowcount DESC ), logbytes_worst AS ( - SELECT TOP 1 'Your worst log bytes range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg log bytes range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_log_bytes_mb DESC ), tempdb_worst AS ( - SELECT TOP 1 'Your worst tempdb range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + SELECT TOP (1) 'Your worst avg tempdb range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg FROM worsts ORDER BY worsts.total_avg_tempdb_space DESC + )/*maxes*/, + max_duration_worst AS ( + SELECT TOP (1) 'Your worst max duration range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_duration_ms DESC + ), + max_cpu_worst AS ( + SELECT TOP (1) 'Your worst max cpu range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_cpu_time_ms DESC + ), + max_logical_reads_worst AS ( + SELECT TOP (1) 'Your worst max logical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_logical_io_reads_mb DESC + ), + max_physical_reads_worst AS ( + SELECT TOP (1) 'Your worst max physical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_physical_io_reads_mb DESC + ), + max_logical_writes_worst AS ( + SELECT TOP (1) 'Your worst max logical write range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_logical_io_writes_mb DESC + ), + max_memory_worst AS ( + SELECT TOP (1) 'Your worst max memory range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_query_max_used_memory_mb DESC + ), + max_logbytes_worst AS ( + SELECT TOP (1) 'Your worst max log bytes range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_log_bytes_mb DESC + ), + max_tempdb_worst AS ( + SELECT TOP (1) 'Your worst max tempdb range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg + FROM worsts + ORDER BY worsts.total_max_tempdb_space DESC ) INSERT #warning_results ( CheckID, Priority, FindingsGroup, Finding, URL, Details ) - SELECT 1002, 255, 'Worsts', 'Worst Duration', 'N/A', duration_worst.msg + /*averages*/ + SELECT 1002, 255, 'Worsts', 'Worst Avg Duration', 'N/A', duration_worst.msg FROM duration_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst CPU', 'N/A', cpu_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg CPU', 'N/A', cpu_worst.msg FROM cpu_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Reads', 'N/A', logical_reads_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg Logical Reads', 'N/A', logical_reads_worst.msg FROM logical_reads_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Physical Reads', 'N/A', physical_reads_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg Physical Reads', 'N/A', physical_reads_worst.msg FROM physical_reads_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Writes', 'N/A', logical_writes_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg Logical Writes', 'N/A', logical_writes_worst.msg FROM logical_writes_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Memory', 'N/A', memory_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg Memory', 'N/A', memory_worst.msg FROM memory_worst UNION ALL SELECT 1002, 255, 'Worsts', 'Worst Row Counts', 'N/A', rowcount_worst.msg FROM rowcount_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Log Bytes', 'N/A', logbytes_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg Log Bytes', 'N/A', logbytes_worst.msg FROM logbytes_worst UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst tempdb', 'N/A', tempdb_worst.msg + SELECT 1002, 255, 'Worsts', 'Worst Avg tempdb', 'N/A', tempdb_worst.msg FROM tempdb_worst + UNION ALL + /*maxes*/ + SELECT 1002, 255, 'Worsts', 'Worst Max Duration', 'N/A', max_duration_worst.msg + FROM max_duration_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max CPU', 'N/A', max_cpu_worst.msg + FROM max_cpu_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max Logical Reads', 'N/A', max_logical_reads_worst.msg + FROM max_logical_reads_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max Physical Reads', 'N/A', max_physical_reads_worst.msg + FROM max_physical_reads_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max Logical Writes', 'N/A', max_logical_writes_worst.msg + FROM max_logical_writes_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max Memory', 'N/A', max_memory_worst.msg + FROM max_memory_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max Log Bytes', 'N/A', max_logbytes_worst.msg + FROM max_logbytes_worst + UNION ALL + SELECT 1002, 255, 'Worsts', 'Worst Max tempdb', 'N/A', max_tempdb_worst.msg + FROM max_tempdb_worst OPTION (RECOMPILE); @@ -4595,7 +5921,7 @@ BEGIN URL, Details, CheckID - ORDER BY Priority ASC, CheckID ASC + ORDER BY Priority ASC, FindingsGroup, Finding, CheckID ASC OPTION (RECOMPILE); @@ -4766,7 +6092,7 @@ EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @DurationFilt --Look for a stored procedure name (that doesn't exist!) EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'blah' ---Look for a stored procedure name that does (at least On My Computer®) +--Look for a stored procedure name that does (at least On My Computer®) EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'UserReportExtended' --Look for failed queries diff --git a/Deprecated/sp_BlitzRS.sql b/Deprecated/sp_BlitzRS.sql deleted file mode 100644 index bd8ed7fef..000000000 --- a/Deprecated/sp_BlitzRS.sql +++ /dev/null @@ -1,967 +0,0 @@ -USE [ReportServer] -GO -/****** Object: StoredProcedure [dbo].[sp_BlitzRS] Script Date: 9/9/2014 10:02:33 PM ******/ -SET ANSI_NULLS ON -GO -SET QUOTED_IDENTIFIER ON -GO - -IF OBJECT_ID('dbo.sp_BlitzRS') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzRS AS RETURN 0;') -GO - - -ALTER PROCEDURE [dbo].[sp_BlitzRS] -@WhoGetsWhat TINYINT = 0, -@Help TINYINT = 0 -WITH RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -IF @Help = 1 PRINT ' -/* -sp_BlitzRS from http://FirstResponderKit.org - -Description: Displays information about a single SQL Server Reporting -Services instance based on the ReportServer database contents. Run this -against the server with the ReportServer database (usually, but not -necessarily the server running the SSRS service). - -Output: One result set is presented that contains data from the ReportServer -database tables. Other result sets can be included via parameter. - -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. - -Known limitations of this version: -- This query will not run on SQL Server 2005. -- Looks for the ReportServer database only. -- May run for several minutes if the catalog is large (1,000+ items). -- Will not identify data-driven subscription source query information - such as tables, parameters, or recipients. - -Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - -Changes in v1.0 - YYYY/MM/DD - - Switched to MIT licensing. - - Added @Help parameter. - -v0.92 - 2014-09-22 - - Fixed issue where query would try to parse images and other objects as XML. - - Lengthened dataset CommandText and subscription Parameter variables. - -v0.91 - 2014-09-17 - - Corrected inconsistent case-sensitivity - -v0.9 - 2014-09-16 - - Changed support links, added credit. - -v0.8 - 2014-08-29 - - Added "who gets what" parameter and query. - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -' -IF OBJECT_ID('tempdb..#BlitzRSResults') IS NOT NULL - DROP TABLE #BlitzRSResults; - -CREATE TABLE #BlitzRSResults - ( - ID INT IDENTITY(1, 1) , - CheckID INT , - [Priority] TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - URL VARCHAR(200) , - Details NVARCHAR(4000) - ) - -/* Parameter variables */ - -DECLARE @DeadReportThreshold SMALLINT = 0 - -/* -------------------------------------------------------------------- -CHECK #1 -Find reports that don't have any stored procs or shared datasets. -------------------------------------------------------------------- -*/ - -DECLARE @DataSetContent TABLE - ( - ItemID UNIQUEIDENTIFIER , - ReportName NVARCHAR(200) , - DataSetName NVARCHAR(200) , - DataSourceName NVARCHAR(200) , - CommandText NVARCHAR(MAX) , - SharedDataSetRef NVARCHAR(80) - ); -DECLARE @DataSets TABLE - ( - ItemID UNIQUEIDENTIFIER , - ReportName NVARCHAR(200) , - Content XML - ); - -INSERT @DataSets - ( ItemID , - ReportName , - Content - ) - SELECT ItemID , - [Name] AS ReportName , - CAST(CAST(Content AS VARBINARY(MAX)) AS XML) - FROM dbo.Catalog - WHERE [Type] = 2; - ; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition' AS p - , 'http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition' AS p1) -INSERT @DataSetContent - ( ItemID, ReportName, DataSetName, DataSourceName, CommandText, SharedDataSetRef ) -SELECT ItemID , ReportName, - AOP.Params.value('@Name', 'Varchar(800)') AS 'PDStName', - AOP.Params.value('p:Query[1]/p:DataSourceName[1]', 'Varchar(800)') AS 'PName', - AOP.Params.value('p:Query[1]/p:CommandText[1]', 'Varchar(800)') AS 'PCmd', - AOP.Params.value('p:SharedDataSet[1]/p:SharedDataSetReference[1]', 'Varchar(800)') AS 'PSDSR' - FROM @DataSets - CROSS APPLY Content.nodes('/p:Report/p:DataSets/p:DataSet') AS AOP ( Params ) -UNION -SELECT ItemID , ReportName, AOP.Params.value('@Name', 'Varchar(800)') AS 'PDStName', - AOP.Params.value('p1:Query[1]/p1:DataSourceName[1]', 'Varchar(800)') AS 'PName', - AOP.Params.value('p1:Query[1]/p1:CommandText[1]', 'Varchar(800)') AS 'PCmd', - AOP.Params.value('p1:SharedDataSet[1]/p1:SharedDataSetReference[1]', 'Varchar(800)') AS 'PSDSR' - FROM @DataSets - CROSS APPLY Content.nodes('/p1:Report/p1:DataSets/p1:DataSet') AS AOP ( Params ) - - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 1 , - 100 , - 'Maintainability' , - 'Reports with 100% inline T-SQL' , - 'http://brentozar.com/BlitzRS/inlinetsql' , - 'The datasets in the "' + ReportName - + '" report all use inline T-SQL, rather that stored procs or shared datasets. This can make datasets harder to tune and schema changes harder to adapt to.' - FROM @DataSetContent - GROUP BY ReportName - HAVING SUM(CASE WHEN CHARINDEX('SELECT', CommandText, 0) > 0 THEN 1 - ELSE 0 - END) > 1 - AND SUM(CASE WHEN CHARINDEX('SELECT', CommandText, 0) = 0 - AND SharedDataSetRef IS NULL THEN 1 - ELSE 0 - END) - + SUM(CASE WHEN SharedDataSetRef IS NOT NULL THEN 1 - ELSE 0 - END) = 0 - - -/* -------------------------------------------------------------------- -CHECK #2 -Finds data dumps (large, featureless reports) in disguise. -------------------------------------------------------------------- -*/ - - -DECLARE @ContentFeatures TABLE - ( - ItemID UNIQUEIDENTIFIER , - ReportName NVARCHAR(200) , - HasCharts BIT , - HasGauges BIT , - HasMaps BIT , - HasSubreports BIT , - HasDrillthroughs BIT , - HasToggles BIT - ); -DECLARE @ContentXML TABLE - ( - ItemID UNIQUEIDENTIFIER , - ReportName NVARCHAR(200) , - Content XML - ); - -INSERT @ContentXML - ( ItemID , - ReportName , - Content - ) - SELECT ItemID , - [Name] AS ReportName , - CAST(CAST(Content AS VARBINARY(MAX)) AS XML) - FROM dbo.Catalog - WHERE [Type] = 2; - ; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition' AS p - , 'http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition' AS p1) -INSERT @ContentFeatures - ( ItemID, ReportName, HasCharts, HasGauges, HasMaps, HasSubreports, HasDrillthroughs, HasToggles ) -SELECT ItemID , ReportName, - CASE WHEN Content.exist('//p:Chart') = 1 THEN 1 ELSE 0 END AS HasCharts, - CASE WHEN Content.exist('//p:GaugePanel') = 1 THEN 1 ELSE 0 END AS HasGauges, - CASE WHEN Content.exist('//p:Map') = 1 THEN 1 ELSE 0 END AS HasMaps, - CASE WHEN Content.exist('//p:Subreport') = 1 THEN 1 ELSE 0 END AS HasSubreports, - CASE WHEN Content.exist('//p:Drillthrough') = 1 THEN 1 ELSE 0 END AS HasDrillthroughs, - CASE WHEN Content.exist('//p:ToggleItem') = 1 THEN 1 ELSE 0 END AS HasToggles - FROM @ContentXML - WHERE CHARINDEX('http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition', CAST(Content AS NVARCHAR(MAX))) > 0 -UNION -SELECT ItemID , ReportName, - CASE WHEN Content.exist('//p1:Chart') = 1 THEN 1 ELSE 0 END , - CASE WHEN Content.exist('//p1:GaugePanel') = 1 THEN 1 ELSE 0 END , - CASE WHEN Content.exist('//p1:Map') = 1 THEN 1 ELSE 0 END , - CASE WHEN Content.exist('//p1:Subreport') = 1 THEN 1 ELSE 0 END , - CASE WHEN Content.exist('//p1:Drillthrough') = 1 THEN 1 ELSE 0 END , - CASE WHEN Content.exist('//p1:ToggleItem') = 1 THEN 1 ELSE 0 END - FROM @ContentXML -WHERE CHARINDEX('http://schemas.microsoft.com/sqlserver/reporting/2010/01/reportdefinition', CAST(Content AS NVARCHAR(MAX))) > 0 - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 2 , - 50 , - 'Performance' , - 'Data Dumps in Disguise' , - 'http://brentozar.com/BlitzRS/datadumps' , - 'The "' + ReportName - + '" report appears to be a data dump with no special SSRS features. If your server is under pressure, consider delivering this data through other means.' - FROM ExecutionLogStorage els WITH ( NOLOCK ) - JOIN Catalog C ON ( els.ReportID = C.ItemID ) - JOIN @ContentFeatures AS cf ON cf.ItemID = C.ItemID - WHERE [RowCount] >= 1000 - AND TimeDataRetrieval + TimeProcessing + TimeRendering > 5000 - AND ReportAction = 1 - AND RequestType IN ( 1, 2 ) - AND HasCharts = 0 - AND HasGauges = 0 - AND HasMaps = 0 - AND HasSubreports = 0 - AND HasDrillthroughs = 0 - AND HasToggles = 0; - - -/* -------------------------------------------------------------------- -CHECK #3 -Find reports needing a performance tune-up. -------------------------------------------------------------------- -*/ - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 3 AS CheckID , - 60 AS [Priority] , - 'Performance' AS FindingsGroup , - 'Slow Datasets' AS Finding , - 'http://brentozar.com/BlitzRS/slowdataset' AS URL , - 'The "' + c.Path - + '" report waits an average of 5+ seconds for query data to come back. There may be a query here that needs tuning. ' AS Details - FROM ExecutionLogStorage AS els - JOIN Catalog AS c ON ( els.ReportID = c.ItemID ) - WHERE ReportAction = 1 - AND RequestType IN ( 1, 2 ) - GROUP BY c.Path - HAVING AVG(TimeDataRetrieval) > 5000 - - -/* -------------------------------------------------------------------- -CHECK #4 -Find reports that used to run but have failed from a certain point -forward. -------------------------------------------------------------------- -*/ -; -WITH cte - AS ( SELECT ReportID , - c.Name AS ReportName , - MAX(CASE WHEN [Status] = 'rsSuccess' THEN TimeStart - END) AS LastKnownSuccess - FROM dbo.ExecutionLogStorage AS els - JOIN dbo.Catalog AS c ON c.ItemID = els.ReportID - GROUP BY ReportID , - c.Name - ) - INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 4 AS CheckID , - 40 AS [Priority] , - 'Reliability' AS FindingsGroup , - '"I''ve fallen and I can''t get up!"' AS Finding , - 'http://brentozar.com/BlitzRS/reportdown' AS URL , - 'The "' + cte.ReportName - + '" report last ran successfully at ' - + CAST(cte.LastKnownSuccess AS VARCHAR(24)) - + '. Since then, it''s had ' - + CAST(COUNT([status]) AS VARCHAR(20)) - + ' failed executions.' AS Details - FROM dbo.ExecutionLogStorage AS els - JOIN cte ON cte.ReportID = els.ReportID - AND els.TimeStart >= DATEADD(dd, - @DeadReportThreshold, - cte.LastKnownSuccess) - WHERE Status != 'rsSuccess' - GROUP BY cte.ReportName , - cte.LastKnownSuccess - -/* -------------------------------------------------------------------- -CHECK #5 -Find altered role permissions and affected users. -------------------------------------------------------------------- -*/ - -IF OBJECT_ID('tempdb..#RoleDefault') IS NOT NULL - DROP TABLE #RoleDefault; - -CREATE TABLE #RoleDefault - ( - RoleName NVARCHAR(32) , - RoleFlags TINYINT , - TaskMask NVARCHAR(32) - ) -INSERT #RoleDefault - ( RoleName, RoleFlags, TaskMask ) -VALUES ( N'Browser', 0, N'0010101001000100' ) - , - ( N'Content Manager', 0, N'1111111111111111' ) - , - ( N'Model Item Browser', 2, N'1' ) - , - ( N'My Reports', 0, N'0111111111011000' ) - , - ( N'Publisher', 0, N'0101010100001010' ) - , - ( N'Report Builder', 0, N'0010101001000101' ) - , - ( N'System Administrator', 1, N'110101011' ) - , - ( N'System User', 1, N'001010001' ) - -IF OBJECT_ID('tempdb..#RoleDefaultTaskMask') IS NOT NULL - DROP TABLE #RoleDefaultTaskMask; - -CREATE TABLE #RoleDefaultTaskMask - ( - RoleName NVARCHAR(32) , - RoleFlags TINYINT , - TaskMaskPos TINYINT , - TaskMaskValue TINYINT - ) -INSERT #RoleDefaultTaskMask - ( RoleName , - RoleFlags , - TaskMaskPos , - TaskMaskValue - ) - SELECT RoleName , - RoleFlags , - upvt.TaskMaskPos , - upvt.TaskMask AS TaskMaskValue - FROM ( SELECT RoleName , - RoleFlags , - CAST(SUBSTRING(TaskMask, 1, 1) AS CHAR(1)) AS [1] , - CAST(SUBSTRING(TaskMask, 2, 1) AS CHAR(1)) AS [2] , - CAST(SUBSTRING(TaskMask, 3, 1) AS CHAR(1)) AS [3] , - CAST(SUBSTRING(TaskMask, 4, 1) AS CHAR(1)) AS [4] , - CAST(SUBSTRING(TaskMask, 5, 1) AS CHAR(1)) AS [5] , - CAST(SUBSTRING(TaskMask, 6, 1) AS CHAR(1)) AS [6] , - CAST(SUBSTRING(TaskMask, 7, 1) AS CHAR(1)) AS [7] , - CAST(SUBSTRING(TaskMask, 8, 1) AS CHAR(1)) AS [8] , - CAST(SUBSTRING(TaskMask, 9, 1) AS CHAR(1)) AS [9] , - CAST(SUBSTRING(TaskMask, 10, 1) AS CHAR(1)) AS [10] , - CAST(SUBSTRING(TaskMask, 11, 1) AS CHAR(1)) AS [11] , - CAST(SUBSTRING(TaskMask, 12, 1) AS CHAR(1)) AS [12] , - CAST(SUBSTRING(TaskMask, 13, 1) AS CHAR(1)) AS [13] , - CAST(SUBSTRING(TaskMask, 14, 1) AS CHAR(1)) AS [14] , - CAST(SUBSTRING(TaskMask, 15, 1) AS CHAR(1)) AS [15] , - CAST(SUBSTRING(TaskMask, 16, 1) AS CHAR(1)) AS [16] - FROM #RoleDefault - ) AS r UNPIVOT -( TaskMask FOR TaskMaskPos IN ( [1], [2], [3], [4], [5], [6], [7], [8], [9], - [10], [11], [12], [13], [14], [15], [16] ) ) AS upvt - WHERE TaskMask != ' ' - -IF OBJECT_ID('tempdb..#RolePermissions') IS NOT NULL - DROP TABLE #RolePermissions; - -CREATE TABLE #RolePermissions - ( - RoleFlag TINYINT , - TaskMaskPos TINYINT , - Permission NVARCHAR(80) - ) -INSERT #RolePermissions - ( RoleFlag, TaskMaskPos, Permission ) -VALUES ( 0, 1, N'Set security for individual items' ) - , - ( 0, 2, N'Create linked reports' ) - , - ( 0, 3, N'View reports' ) - , - ( 0, 4, N'Manage reports' ) - , - ( 0, 5, N'View resources' ) - , - ( 0, 6, N'Manage resources' ) - , - ( 0, 7, N'View folders' ) - , - ( 0, 8, N'Manage folders' ) - , - ( 0, 9, N'Manage report history' ) - , - ( 0, 10, N'Manage individual subscriptions' ) - , - ( 0, 11, N'Manage all subscriptions' ) - , - ( 0, 12, N'View data sources' ) - , - ( 0, 13, N'Manage data sources' ) - , - ( 0, 14, N'View models' ) - , - ( 0, 15, N'Manage models' ) - , - ( 0, 16, N'Consume reports' ) - , - ( 1, 1, N'Manage roles' ) - , - ( 1, 2, N'Manage report server security' ) - , - ( 1, 3, N'View report server properties' ) - , - ( 1, 4, N'Manage report server properties' ) - , - ( 1, 5, N'View shared schedules' ) - , - ( 1, 6, N'Manage shared schedules' ) - , - ( 1, 7, N'Generate events' ) - , - ( 1, 8, N'Manage jobs' ) - , - ( 1, 9, N'Execute Report Definitions' ) - -IF OBJECT_ID('tempdb..#RoleTaskMask') IS NOT NULL - DROP TABLE #RoleTaskMask; - -CREATE TABLE #RoleTaskMask - ( - RoleName NVARCHAR(260) , - RoleFlags TINYINT , - TaskMaskPos TINYINT , - TaskMaskValue TINYINT - ) - -INSERT #RoleTaskMask - ( RoleName , - RoleFlags , - TaskMaskPos , - TaskMaskValue - ) - SELECT RoleName , - RoleFlags , - upvt.TaskMaskPos , - upvt.TaskMask AS TaskMaskValue - FROM ( SELECT RoleName , - RoleFlags , - SUBSTRING(TaskMask, 1, 1) AS [1] , - SUBSTRING(TaskMask, 2, 1) AS [2] , - SUBSTRING(TaskMask, 3, 1) AS [3] , - SUBSTRING(TaskMask, 4, 1) AS [4] , - SUBSTRING(TaskMask, 5, 1) AS [5] , - SUBSTRING(TaskMask, 6, 1) AS [6] , - SUBSTRING(TaskMask, 7, 1) AS [7] , - SUBSTRING(TaskMask, 8, 1) AS [8] , - SUBSTRING(TaskMask, 9, 1) AS [9] , - SUBSTRING(TaskMask, 10, 1) AS [10] , - SUBSTRING(TaskMask, 11, 1) AS [11] , - SUBSTRING(TaskMask, 12, 1) AS [12] , - SUBSTRING(TaskMask, 13, 1) AS [13] , - SUBSTRING(TaskMask, 14, 1) AS [14] , - SUBSTRING(TaskMask, 15, 1) AS [15] , - SUBSTRING(TaskMask, 16, 1) AS [16] - FROM dbo.Roles - ) AS r UNPIVOT -( TaskMask FOR TaskMaskPos IN ( [1], [2], [3], [4], [5], [6], [7], [8], [9], - [10], [11], [12], [13], [14], [15], [16] ) ) AS upvt - WHERE TaskMask != ' ' - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 4 AS CheckID , - 20 AS [Priority] , - 'Security' AS FindingsGroup , - 'Non-Default Role Permissions' AS Finding , - 'http://brentozar.com/BlitzRS/roledefaults' AS URL , - 'The user ' + u.UserName + ' has ' - + CASE WHEN r.TaskMaskValue = 0 THEN 'been denied ' - ELSE '' - END + 'permission to "' + rp.Permission + '" because the ' - + r.RoleName - + ' role''s default security settings have been changed.' AS Details - FROM #RoleTaskMask AS r - JOIN #RoleDefaultTaskMask AS rd ON rd.RoleName = r.RoleName - AND rd.RoleFlags = r.RoleFlags - AND rd.TaskMaskPos = r.TaskMaskPos - JOIN #RolePermissions AS rp ON rp.RoleFlag = r.RoleFlags - AND rp.TaskMaskPos = r.TaskMaskPos - JOIN dbo.Roles AS ro ON ro.RoleName COLLATE DATABASE_DEFAULT = r.RoleName - LEFT JOIN dbo.PolicyUserRole AS pur ON ro.RoleID = pur.RoleID - LEFT JOIN dbo.Users AS u ON pur.UserID = u.UserID - WHERE r.TaskMaskValue != rd.TaskMaskValue - -/* -------------------------------------------------------------------- -CHECK #6 -Find accounts with administrator-like privileges. -------------------------------------------------------------------- -*/ - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 5 AS CheckID , - 30 AS [Priority] , - 'Security' AS FindingsGroup , - 'Accounts with admin privileges' AS Finding , - 'http://brentozar.com/BlitzRS/admins' AS URL , - 'The user ' + u.UserName + ' belongs to the ' + ro.RoleName - + ' role. This role has some administrator-like privileges related to ' - + CASE WHEN ro.RoleName = 'Content Manager' - THEN 'reports, subscriptions, and datasets.' - WHEN ro.RoleName = 'System Administrator' - THEN 'schedules and security.' - ELSE 'something, but I'' not sure what.' - END AS Details - FROM dbo.Roles AS ro - LEFT JOIN dbo.PolicyUserRole AS pur ON ro.RoleID = pur.RoleID - LEFT JOIN dbo.Users AS u ON pur.UserID = u.UserID - WHERE ro.RoleName IN ( 'Content Manager', 'System Administrator' ) - -/* ------------------------------------------------------- */ -/* SECTION IS FOR LATER USE */ - - ---DECLARE @CatalogParams TABLE --- ( --- ItemID UNIQUEIDENTIFIER , --- ParamName NVARCHAR(80), --- DefaultValue NVARCHAR(80) --- ); ---DECLARE @Catalog TABLE --- ( --- ItemID UNIQUEIDENTIFIER , --- ParamField XML --- ); - ---INSERT @Catalog --- ( ItemID , --- ParamField --- ) --- SELECT ItemID , --- CAST([Parameter] AS XML) --- FROM dbo.Catalog --- WHERE [Type] = 2; --- ; ---INSERT @CatalogParams --- ( ItemID, ParamName, DefaultValue ) ---SELECT ItemID , --- AOP.Params.value('Name[1]', 'Varchar(800)') AS 'PName', --- AOP.Params.value('DefaultValues[1]/Value[1]', 'Varchar(800)') AS 'PVal' --- FROM @Catalog --- CROSS APPLY ParamField.nodes('//Parameters/Parameter') AS AOP ( Params ) - ---SELECT * FROM @CatalogParams - -/* END SECTION */ -/*---------------------------------------------------------*/ - -/* -------------------------------------------------------------------- -CHECK #7 -Find reports that may do better with snapshots. -------------------------------------------------------------------- -*/ -; -WITH cte - AS ( SELECT COUNT(LogEntryId) AS RenderCount , - ReportID - FROM dbo.ExecutionLogStorage AS els - WHERE TimeDataRetrieval + TimeProcessing + TimeDataRetrieval >= 5000 - AND RequestType IN (0, 1) - AND els.[Format] IS NOT NULL - GROUP BY ReportID - ) - INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 7 AS CheckID , - 70 AS [Priority] , - 'Performance' AS FindingsGroup , - 'Candidates for snapshots' AS Finding , - 'http://brentozar.com/BlitzRS/snapshots' AS URL , - 'When the ' + c.[Name] + ' report runs over 5 seconds, ' - + CAST(CAST(( COUNT(els.LogEntryId) * 100.0 ) - / ( cte.RenderCount * 1.0 ) AS DECIMAL(9, 0)) AS NVARCHAR(3)) - + '% of the time it uses a distinct format and set of parameters. This makes it a candidate for rendering from a snapshot.' - FROM dbo.ExecutionLogStorage AS els - JOIN cte ON cte.ReportID = els.ReportID - JOIN dbo.Catalog AS c ON c.itemid = els.ReportID - WHERE TimeDataRetrieval + TimeProcessing + TimeDataRetrieval >= 5000 - AND cte.RenderCount >= 20 - AND els.RequestType IN (0, 1) - AND els.[Format] IS NOT NULL - GROUP BY c.[Name] , - CAST(els.[Parameters] AS NVARCHAR(2000)) , - cte.RenderCount, - els.[Format] - HAVING CAST(( COUNT(els.LogEntryId) * 100.0 ) / ( cte.RenderCount - * 1.0 ) AS DECIMAL(9, - 0)) > 20 - ORDER BY COUNT(LogEntryId) DESC - -/* -------------------------------------------------------------------- -CHECK #8 -Find reports that may be worth caching. -------------------------------------------------------------------- -*/ - -; -WITH cte - AS ( SELECT ReportID , - COUNT(LogEntryId) AS RenderCount - FROM dbo.ExecutionLogStorage - WHERE RequestType IN ( 1, 2 ) - GROUP BY ReportID - ) - INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 8 AS CheckID , - 80 AS [Priority] , - 'Performance' AS FindingsGroup , - 'Candidates for caching' AS Finding , - 'http://brentozar.com/BlitzRS/caching' AS URL , - 'When the ' + c.[Name] + ' report runs, ' - + CAST(CAST(( COUNT(els.LogEntryId) * 100.0 ) - / ( cte.RenderCount * 1.0 ) AS DECIMAL(9, 0)) AS NVARCHAR(3)) - + '% of the time it starts during the ' - + CAST(DATEPART(hh, TimeStart) AS NVARCHAR(12)) - + ':00 hour. Caching these reports can improve performance during this hour.' - FROM dbo.ExecutionLogStorage AS els - JOIN cte ON cte.ReportID = els.ReportID - JOIN dbo.Catalog AS c ON c.itemid = els.ReportID - WHERE RequestType IN ( 1, 2 ) - AND cte.RenderCount >= 20 - GROUP BY c.[Name] , - DATEPART(hh, TimeStart) , - cte.RenderCount - HAVING CAST(( COUNT(els.LogEntryId) * 100.0 ) / ( cte.RenderCount - * 1.0 ) AS DECIMAL(9, - 0)) >= 10 - -/* -------------------------------------------------------------------- -CHECK #9 -See when the ReportServer database was last backed up. -------------------------------------------------------------------- -*/ - -INSERT INTO #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 10 AS CheckID , - 10 AS [PRIORITY] , - 'Backup' AS FindingsGroup , - 'Backups Not Performed Recently' AS Finding , - 'http://brentozar.com/BlitzRS/nobackup' AS URL , - 'Database ' + d.Name + ' last backed up: ' - + CAST(COALESCE(MAX(b.backup_finish_date), ' never ') AS VARCHAR(200)) AS Details - FROM master.sys.databases d - LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'D' - AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ - WHERE d.database_id <> 2 /* Bonus points if you know what that means */ - AND d.state <> 1 /* Not currently restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name = 'ReportServer' - /* - The above NOT IN filters out the databases we're not supposed to check. - */ - GROUP BY d.name - HAVING MAX(b.backup_finish_date) <= DATEADD(dd, -7, GETDATE()); - -/* -------------------------------------------------------------------- -CHECK #10 -Find who has a report subscription where the recipient is explicitly -stated (not in a data-driven result set). -------------------------------------------------------------------- -*/ - -IF OBJECT_ID('tempdb..#numbers') IS NOT NULL - DROP TABLE #numbers; -IF OBJECT_ID('tempdb..#parms') IS NOT NULL - DROP TABLE #parms; - -/* Create a numbers table to cycle through number positions */ - -CREATE TABLE #numbers ( n SMALLINT ) - -DECLARE @x SMALLINT = 0 -DECLARE @sql NVARCHAR(MAX) - -SET @sql = N'INSERT #numbers VALUES ' -WHILE @x < 800 - BEGIN - SET @sql = @sql + '(' + CAST(@x AS VARCHAR(3)) + '),'; - SET @x = @x + 1 - END - -SET @sql = @sql + '(' + CAST(@x AS VARCHAR(3)) + ')'; - -/* Populate the numbers table up to 800 */ -EXEC sp_executesql @sql; - -/* Fill a parameters table by changing ExtensionSettings to XML, -then parsing it, taking only the {to, cc, bcc} set. - -*/ - -CREATE TABLE #parms - ( - SubscriptionID UNIQUEIDENTIFIER , - parm NVARCHAR(3200) , - pos SMALLINT - ); -WITH cte - AS ( SELECT SubscriptionID , - CASE WHEN RIGHT(s.value('(Value/text())[1]', - 'nvarchar(800)'), 1) = ';' - THEN LEFT(s.value('(Value/text())[1]', - 'nvarchar(800)'), - LEN(s.value('(Value/text())[1]', - 'nvarchar(800)')) - 1) - ELSE s.value('(Value/text())[1]', 'nvarchar(800)') - END AS parm - FROM ( SELECT SubscriptionID , - CAST(ExtensionSettings AS XML) AS ExtSettings - FROM dbo.Subscriptions - WHERE DeliveryExtension = 'Report Server Email' - ) AS subs - CROSS APPLY subs.ExtSettings.nodes('/ParameterValues/ParameterValue') - AS SubsX ( s ) - WHERE s.value('(Name/text())[1]', 'nvarchar(800)') IN ( 'TO', - 'CC', 'BCC' ) - ) - INSERT #parms - ( SubscriptionID , - parm , - pos - ) - SELECT DISTINCT - cte.SubscriptionID , - parm , - n AS pos - FROM cte - CROSS JOIN #numbers - WHERE ( CHARINDEX(';', parm, n) = n - OR ( CHARINDEX(';', parm, n) = 0 - AND n = 0 - ) - OR n = 0 - ) - AND LEN(parm) != n - -INSERT #BlitzRSResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 10 AS CheckID , - 150 AS [Priority] , - 'Informational' AS FindingsGroup , - 'E-mail accounts with subscriptions' AS Finding , - 'http://brentozar.com/BlitzRS/emailsubs' AS URL , - 'The e-mail address "' - + CASE WHEN pos = 0 - AND CHARINDEX(';', parm, pos) > 0 - THEN SUBSTRING(parm, pos + 1, - CHARINDEX(';', parm, pos) - 1) - WHEN pos = 0 - AND CHARINDEX(';', parm, pos) = 0 THEN parm - ELSE SUBSTRING(parm, pos + 1, CHARINDEX(';', parm, pos)) - END + '" is included in ' - + CAST(COUNT(p.SubscriptionID) AS NVARCHAR(8)) - + ' subscriptions' - + CASE WHEN SUM(CASE WHEN s.DataSettings IS NULL THEN 0 - ELSE 1 - END) > 0 - THEN ', including at least one data-driven subscription' - ELSE '' - END + '.' - FROM #parms AS p - JOIN dbo.Subscriptions AS s ON s.SubscriptionID = p.SubscriptionID - JOIN dbo.Catalog AS c ON c.ItemID = s.Report_OID - JOIN dbo.ReportSchedule AS rs ON rs.ReportID = s.Report_OID - AND rs.SubscriptionID = s.SubscriptionID - JOIN dbo.Schedule AS sh ON sh.ScheduleID = rs.ScheduleID - GROUP BY CASE WHEN pos = 0 - AND CHARINDEX(';', parm, pos) > 0 - THEN SUBSTRING(parm, pos + 1, - CHARINDEX(';', parm, pos) - 1) - WHEN pos = 0 - AND CHARINDEX(';', parm, pos) = 0 THEN parm - ELSE SUBSTRING(parm, pos + 1, CHARINDEX(';', parm, pos)) - END - ORDER BY SUM(CASE WHEN s.DataSettings IS NULL THEN 0 - ELSE 1 - END) DESC , - CASE WHEN pos = 0 - AND CHARINDEX(';', parm, pos) > 0 - THEN SUBSTRING(parm, pos + 1, - CHARINDEX(';', parm, pos) - 1) - WHEN pos = 0 - AND CHARINDEX(';', parm, pos) = 0 THEN parm - ELSE SUBSTRING(parm, pos + 1, CHARINDEX(';', parm, pos)) - END - - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - INSERT INTO #BlitzRSResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Brent Ozar Unlimited' , - 'http://www.BrentOzar.com/blitzrs/' , - 'Thanks from the Brent Ozar Unlimited team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.' - ); - - - -SELECT [Priority] , - FindingsGroup , - Finding , - URL , - Details, - CheckID -FROM #BlitzRSResults -ORDER BY [Priority] , - FindingsGroup , - Finding - - -IF @WhoGetsWhat = 1 - SELECT - p.SubscriptionID - , s.[Description] as SubscriptionName - , c.[Name] as ReportName - , sh.Name as ScheduleName - , sh.LastRunTime - , s.LastStatus - , sh.NextRunTime - , CASE WHEN pos = 0 AND CHARINDEX(';', parm, pos) > 0 THEN SUBSTRING(parm, pos + 1, CHARINDEX(';', parm, pos) -1) - WHEN pos = 0 AND CHARINDEX(';', parm, pos) = 0 THEN parm - ELSE SUBSTRING(parm, pos + 1, CHARINDEX(';', parm, pos)) - END AS EmailAddress - , CASE WHEN s.DataSettings IS NULL THEN 0 ELSE 1 END AS IsDataDrivenSubscription - FROM #parms as p - JOIN dbo.Subscriptions as s on s.SubscriptionID = p.SubscriptionID - join dbo.Catalog as c on c.ItemID = s.Report_OID - JOIN dbo.ReportSchedule as rs on rs.ReportID = s.Report_OID AND rs.SubscriptionID = s.SubscriptionID - join dbo.Schedule as sh on sh.ScheduleID = rs.ScheduleID - ORDER BY EmailAddress, p.SubscriptionID - -END - diff --git a/Deprecated/sp_BlitzTrace.sql b/Deprecated/sp_BlitzTrace.sql deleted file mode 100644 index a6ca4cc91..000000000 --- a/Deprecated/sp_BlitzTrace.sql +++ /dev/null @@ -1,778 +0,0 @@ -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO -IF OBJECT_ID('dbo.sp_BlitzTrace') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzTrace AS RETURN 0'); -GO - -ALTER PROCEDURE dbo.sp_BlitzTrace - @Debug BIT = 0 , /* 1 prints the statement and won't execute it */ - @SessionId INT = NULL , - @Action VARCHAR(5) = NULL , /* 'start', 'read', 'stop', 'drop'*/ - @TargetPath VARCHAR(528) = NULL, /* Required for 'start'. 'Read' will look for a running sp_BlitzTrace session if not specified.*/ - @TraceRecompiles BIT = 1, - @TraceObjectCreates BIT = 1, - @TraceParallelism BIT = 1, - @TraceSortWarnings BIT = 1, - @TraceStatements BIT = 0, - @MaxFileSizeMB INT = 256, - @TraceExecutionPlansAndKillMyPerformance BIT = 0, /* Non-production environments only */ - @MaxRolloverFiles INT = 4, - @MaxDispatchLatencySeconds INT = 5 /* 0 is unlimited! */, - @Help TINYINT = 0 - -WITH RECOMPILE -AS -IF @Help = 1 PRINT ' -/* -sp_BlitzTrace from http://FirstResponderKit.org - -Description: Starts, stops, and reads Extended Events traces. - ---List running sessions -exec sp_BlitzTrace @Action=''start''; - ---Start a trace for a session. You specify the @SessionID and @TargetPath -exec sp_BlitzTrace @SessionId=52, @TargetPath=''S:\XEvents\Traces\'', @Action=''start''; - ---Stop a session -exec sp_BlitzTrace @Action=''stop''; - ---Read the results. You can move the files to another server and read there by specifying a @TargetPath. -exec sp_BlitzTrace @Action=''read''; - ---Drop the session. This does NOT delete files created in @TargetPath. -exec sp_BlitzTrace @Action=''drop''; - -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. - -Known limitations of this version: - - Extended Events can be hard. - -Unknown limitations of this version: - - Probably a lot. This is one of our lesser-tested scripts. - -Changes in v1.0 - 2016/06/26 - - Switched to MIT licensing. - - Added @Help parameter. - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -' - - DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; - - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SET XACT_ABORT ON; - -BEGIN TRY - - DECLARE @msg NVARCHAR(MAX); - DECLARE @rowcount INT; - DECLARE @v decimal(6,2); - DECLARE @build int; - DECLARE @datestamp VARCHAR(30); - DECLARE @TargetPathFull NVARCHAR(MAX); - DECLARE @filepathXML XML; - DECLARE @filepath VARCHAR(1024); - DECLARE @traceexists BIT = 0; - DECLARE @tracerunning BIT = 0; - - - - /* Validate parameters */ - SET @msg = CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Validatin'' parameters.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @Action NOT IN (N'start', N'read', N'stop', N'drop') OR @Action is NULL - BEGIN - RAISERROR (N'You need to specify a valid @Action for sp_BlitzTrace: ''start'', ''read'', ''stop'', or ''drop''.',16,1) WITH NOWAIT; - END - - IF @Action = N'start' AND @SessionId IS NULL - BEGIN - SELECT ses.session_id, con.last_read, con.last_write, ses.login_name, ses.host_name, ses.program_name, - con.connect_time, con.protocol_type, con.encrypt_option, - con.num_reads, con.num_writes, con.client_net_address, - req.status, req.command, req.wait_type, req.last_wait_type, req.open_transaction_count - FROM sys.dm_exec_sessions as ses - JOIN sys.dm_exec_connections AS con ON - con.session_id=ses.session_id - LEFT JOIN sys.dm_exec_requests AS req ON - con.session_id=req.session_id - WHERE - ses.session_id <> @@SPID - ORDER BY last_read DESC - - RAISERROR (N'sp_BlitzTrace watches just one session, so you have to specify @SessionId. Check out the session list above for some ideas.',16,1) WITH NOWAIT; - END - - IF @MaxDispatchLatencySeconds > 99 - BEGIN - RAISERROR (N'@MaxDispatchLatencySeconds must be 99 or less. 5 is the default. 0 is unlimited latency.',16,1) WITH NOWAIT; - END - - IF @MaxFileSizeMB > 9999 - BEGIN - RAISERROR (N'@MaxFileSizeMB must be 9999 or smaller - 256MB is the default.',16,1) WITH NOWAIT; - END - - IF @MaxRolloverFiles > 99 - BEGIN - RAISERROR (N'@MaxRolloverFiles must be 99 or smaller. 4 is the default.',16,1) WITH NOWAIT; - END - - IF @TargetPath IS NULL AND @Action = N'start' - BEGIN - RAISERROR (N'You gotta give a valid @TargetPath for ''start''.',16,1) WITH NOWAIT; - END - - IF @TargetPath IS NOT NULL AND @Action=N'start' AND RIGHT(@TargetPath, 1) <> N'\' - BEGIN - RAISERROR (N'@TargetPath must be a directory ending in ''\'', like: ''S:\Xevents\''',16,1) WITH NOWAIT; - END - - - IF @TargetPath IS NOT NULL AND @Action='read' AND ( RIGHT(@TargetPath, 1) <> '\' AND RIGHT(@TargetPath, 4) <> '.xel') - BEGIN - RAISERROR (N'To read, @TargetPath must be a directory ending in ''\'', like: ''S:\XEvents\'', or a file ending in .xel, like ''S:\XEvents\sp_BlitzTrace*.xel''',16,1) WITH NOWAIT; - END - - /* Validate transaction state */ - SET @msg = CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Validatin'' transaction state.' - RAISERROR (@msg,0,1) WITH NOWAIT; - IF @@TRANCOUNT > 0 - BEGIN - RAISERROR (N'@@TRANCOUNT > 0 not supported',16,1) WITH NOWAIT; - END - - /* Check version */ - SET @msg = CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Determining SQL Server version.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SELECT @v = SUBSTRING(CAST(SERVERPROPERTY('ProductVersion') as NVARCHAR(128)), 1,CHARINDEX('.', CAST(SERVERPROPERTY('ProductVersion') as NVARCHAR(128))) + 1 ) - - IF @v < N'11' - BEGIN - SET @msg = N'Sad news: most the events sp_BlitzTrace uses are only in SQL Server 2012 and higher-- so it''s not supported on SQL 2008/R2.' - RAISERROR (@msg,16,1) WITH NOWAIT; - RETURN; - END - - - /* Get current trace status */ - SELECT - @traceexists = (CASE WHEN (s.name IS NULL) THEN 0 ELSE 1 END), - @tracerunning = (CASE WHEN (r.create_time IS NULL) THEN 0 ELSE 1 END) - FROM sys.server_event_sessions AS s - LEFT OUTER JOIN sys.dm_xe_sessions AS r ON r.name = s.name - WHERE s.name=N'sp_BlitzTrace' - - - /* We use this to filter sp_BlitzTrace activity out of the results, */ - /* in case you're tracing your own session. */ - /* That's just to make the results less confusing */ - DECLARE @context VARBINARY(128); - SET @context=CAST('sp_BlitzTrace IS THE BEST' as binary); - SET CONTEXT_INFO @context; - - IF @Action = N'start' - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Creating extended events trace.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @traceexists = 1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- A trace named sp_BlitzTrace already exists' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @tracerunning=1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- sp_BlitzTrace is running, so stopping it before we recreate: ALTER EVENT SESSION sp_BlitzTrace ON SERVER STATE = STOP;' - RAISERROR (@msg,0,1) WITH NOWAIT; - - ALTER EVENT SESSION sp_BlitzTrace ON SERVER STATE = STOP; - - SET @tracerunning=0; - END - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Dropping the trace: DROP EVENT SESSION sp_BlitzTrace ON SERVER; ' - RAISERROR (@msg,0,1) WITH NOWAIT; - - DROP EVENT SESSION sp_BlitzTrace ON SERVER; - - SET @traceexists=0; - - END - - IF @traceexists = 0 - BEGIN - SELECT @datestamp = REPLACE(REPLACE( CONVERT(VARCHAR(26),getdate(),126),':','-'),'.','') - SET @TargetPathFull=@TargetPath + N'sp_BlitzTrace-' + @datestamp - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Target path = ' + @TargetPathFull; - RAISERROR (@msg,0,1) WITH NOWAIT; - - - DECLARE @dsql NVARCHAR(MAX); - DECLARE @dsql1 NVARCHAR(MAX)= - N'CREATE EVENT SESSION sp_BlitzTrace ON SERVER - ' + case @TraceStatements when 1 then + N' - ADD EVENT sqlserver.sp_statement_completed ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ADD EVENT sqlserver.sql_statement_completed ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END + case @TraceSortWarnings when 1 then N' - ADD EVENT sqlserver.sort_warning ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END + case @TraceParallelism when 1 then + N' - ADD EVENT sqlserver.degree_of_parallelism ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END + N' - ' + case @TraceObjectCreates when 1 then + N' - ADD EVENT sqlserver.object_created ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END; - - DECLARE @dsql2 NVARCHAR(MAX)= - N'ADD EVENT sqlserver.sql_batch_completed ( - ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' + case @TraceRecompiles when 1 then + N' - ADD EVENT sqlserver.sql_statement_recompile ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END - + case @TraceExecutionPlansAndKillMyPerformance when 1 then + N' - ADD EVENT sqlserver.query_post_execution_showplan ( - ACTION(sqlserver.context_info, sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))), - ' ELSE N'' END + N' - ADD EVENT sqlserver.rpc_completed ( - ACTION(sqlserver.sql_text, sqlserver.query_hash, sqlserver.query_plan_hash) - WHERE ([sqlserver].[session_id]=('+ CAST(@SessionId as NVARCHAR(6)) + N'))) - ADD TARGET package0.event_file(SET filename=''' + @TargetPathFull + N''', - MAX_FILE_SIZE=(' + CAST(@MaxFileSizeMB AS VARCHAR(4)) + N'), - MAX_ROLLOVER_FILES = ' + CAST(@MaxRolloverFiles as VARCHAR(2)) + N') - WITH ( - MAX_MEMORY = 128 MB, - EVENT_RETENTION_MODE = ALLOW_MULTIPLE_EVENT_LOSS, - MAX_DISPATCH_LATENCY = ' + CAST(@MaxDispatchLatencySeconds AS NVARCHAR(2)) + N' SECONDS, - MEMORY_PARTITION_MODE=NONE, - TRACK_CAUSALITY=OFF, - STARTUP_STATE=OFF)'; - - SET @dsql=@dsql1 + @dsql2; - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Creating trace with dynamic SQL. I REGRET NOTHING!!!' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @Debug=1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Debug mode, printing but not executing.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - RAISERROR (@nl,0,1) WITH NOWAIT; - - RAISERROR (@dsql1,0,0) WITH NOWAIT; - RAISERROR (@dsql2,0,0) WITH NOWAIT; - END - ELSE - BEGIN - - EXEC sp_executesql @dsql; - - END - SET @traceexists = 1; - END - - IF @tracerunning = 0 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- ALTER EVENT SESSION sp_BlitzTrace ON SERVER STATE = START;' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @Debug=0 - BEGIN - ALTER EVENT SESSION sp_BlitzTrace ON SERVER STATE = START; - - SET @tracerunning = 1; - END - ELSE - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- @Debug=1, not starting trace;' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - END - END - IF @Action = 'read' - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Reading, processing, and reporting.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @traceexists = 0 - BEGIN - SET @filepath=@TargetPath + N'*.xel'; - - END - - /* Figure out the file name for current trace, if it's running */ - if @tracerunning=1 - BEGIN - SELECT TOP 1 @filepathXML= x.target_data - FROM sys.dm_xe_sessions as se - JOIN sys.dm_xe_session_targets as t on - se.address=t.event_session_address - CROSS APPLY (SELECT cast(t.target_data as XML) AS target_data) as x - WHERE se.name=N'sp_BlitzTrace' - OPTION (RECOMPILE); - - SELECT @filepath=CAST(@filepathXML.query('data(EventFileTarget/File/@name)') AS VARCHAR(1024)) - END - ELSE /* Figure out the file path for the trace if exists and isn't running */ - BEGIN - SELECT TOP 1 @filepath=CAST(f.value AS VARCHAR(1024)) + N'*.xel' - FROM sys.server_event_sessions AS ses - LEFT OUTER JOIN sys.dm_xe_sessions AS running ON - running.name = ses.name - JOIN sys.server_event_session_targets AS t ON - ses.event_session_id = t.event_session_id - and t.package = 'package0' - and t.name='event_file' - and ses.name='sp_BlitzTrace' - JOIN sys.dm_xe_objects AS o ON - t.name = o.name - AND o.object_type='target' - JOIN sys.dm_xe_object_columns AS c ON - t.name = c.object_name AND - c.column_type = 'customizable' AND - c.name='filename' - JOIN sys.server_event_session_fields AS f ON - t.event_session_id = f.event_session_id AND - t.target_id = f.object_id - AND c.name = f.name - END - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Using filepath: ' + @filepath - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @Debug = 0 - BEGIN - CREATE TABLE #sp_BlitzTraceXML ( - event_data XML NOT NULL - ); - - CREATE TABLE #sp_BlitzTraceEvents ( - event_time DATETIME2 NOT NULL, - event_type NVARCHAR(256) NOT NULL, - batch_text VARCHAR(MAX) NULL, - sql_text VARCHAR(MAX) NULL, - [statement] VARCHAR(MAX) NULL, - duration_micros INT NULL, - cpu_micros INT NULL, - physical_reads INT NULL, - logical_reads INT NULL, - writes INT NULL, - row_count INT NULL, - result NVARCHAR(256) NULL, - dop_statement_type SYSNAME NULL, - dop INT NULL, - workspace_memory_grant_kb INT NULL, - object_id INT NULL, - object_type sysname NULL, - object_name sysname NULL, - ddl_phase sysname NULL, - recompile_cause sysname NULL, - sort_warning_type VARCHAR(256) NULL, - query_operation_node_id INT NULL, - query_hash varchar(256) NULL, - query_plan_hash varchar(256) NULL, - estimated_cost bigint NULL, - [showplan_xml] XML NULL, - context_info sysname NULL, - event_data XML NOT NULL - ) - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Populating #sp_BlitzTraceXML...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - INSERT #sp_BlitzTraceXML (event_data) - SELECT - x.event_data - FROM sys.fn_xe_file_target_read_file(@filepath,null,null,null) as xet - CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) as x - OUTER APPLY x.event_data.nodes('//event') AS y(n) OPTION (RECOMPILE); - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Started populating #sp_BlitzTraceEvents...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - INSERT #sp_BlitzTraceEvents (event_time, event_type, batch_text, sql_text, [statement], duration_micros, cpu_micros, physical_reads, - logical_reads, writes, row_count, result, dop_statement_type, dop, workspace_memory_grant_kb, object_id, object_type, - object_name, ddl_phase, recompile_cause, sort_warning_type, query_operation_node_id, query_hash, query_plan_hash, - estimated_cost, [showplan_xml], context_info, event_data) - SELECT - DATEADD(mi, DATEDIFF(mi, GETUTCDATE(), CURRENT_TIMESTAMP), n.value('@timestamp', 'datetime2')) AS event_time, - n.value('@name', 'NVARCHAR(max)') AS event_type, - n.value('(data[@name="batch_text"]/value)[1]', 'varchar(max)') AS batch_text, - n.value('(action[@name="sql_text"]/value)[1]', 'varchar(max)') AS sql_text, - n.value('(data[@name="statement"]/value)[1]', 'varchar(max)') AS [statement], - n.value('(data[@name="duration"]/value)[1]', 'int') AS duration_micros, - n.value('(data[@name="cpu_time"]/value)[1]', 'int') AS cpu_micros, - n.value('(data[@name="physical_reads"]/value)[1]', 'int') AS physical_reads, - n.value('(data[@name="logical_reads"]/value)[1]', 'int') AS logical_reads, - n.value('(data[@name="writes"]/value)[1]', 'int') AS writes, - n.value('(data[@name="row_count"]/value)[1]', 'int') AS row_count, - n.value('(data[@name="result"]/text)[1]', 'varchar(256)') AS result, - - /* Parallelism */ - n.value('(data[@name="statement_type"]/text)[1]', 'varchar(256)') AS dop_statement_type, - n.value('(data[@name="dop"]/value)[1]', 'int') AS dop, - n.value('(data[@name="workspace_memory_grant_kb"]/value)[1]', 'bigint') AS workspace_memory_grant_kb, - - /* Object create comes in pairs, begin and end */ - n.value('(data[@name="object_id"]/value)[1]', 'int') AS [object_id], - n.value('(data[@name="object_type"]/value)[1]', 'varchar(256)') AS object_type, - n.value('(data[@name="object_name"]/value)[1]', 'varchar(256)') AS [object_name], - n.value('(data[@name="ddl_phase"]/value)[1]', 'varchar(256)') AS ddl_phase, - - /* Recompiles */ - n.value('(data[@name="recompile_cause"]/text)[1]', 'varchar(256)') AS recompile_cause, - - /* tempdb spills */ - n.value('(data[@name="sort_warning_type"]/text)[1]', 'varchar(256)') AS sort_warning_type, - n.value('(data[@name="query_operation_node_id"]/value)[1]', 'varchar(256)') AS query_operation_node_id, - - /* query hash and query plan hash */ - n.value('(action[@name="query_hash"]/value)[1]', 'varchar(256)') AS query_hash, - n.value('(action[@name="query_plan_hash"]/value)[1]', 'varchar(256)') AS query_plan_hash, - - /* actual execution plan */ - n.value('(data[@name="estimated_cost"]/value)[1]', 'bigint') AS estimated_cost, - n.query('(data[@name="showplan_xml"]/value/*)[1]') AS [showplan_xml], - - /* Context info, for filtering */ - n.value('(action[@name="context_info"]/value)[1]', 'VARCHAR(MAX)') AS [context_info], - x.event_data as event_data - FROM #sp_BlitzTraceXML AS xet - CROSS APPLY (SELECT CAST(xet.event_data AS XML) AS event_data) as x - OUTER APPLY x.event_data.nodes('//event') AS y(n) - OPTION (RECOMPILE); - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Finished populating #sp_BlitzTraceEvents...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SET @rowcount=@@ROWCOUNT; - - IF @rowcount = 0 - BEGIN - RAISERROR('No rows found in the trace for these events for your session id',0,1) WITH NOWAIT; - END - ELSE - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- ' + CAST(@rowcount AS NVARCHAR(20)) + N' rows inserted into #sp_BlitzTraceEvents.' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - - CREATE CLUSTERED INDEX cx_spBlitzTrace on #sp_BlitzTraceEvents (event_type, event_time) - - DELETE FROM #sp_BlitzTraceEvents - WHERE (@SessionId=@@SPID and [context_info] = '73705f426c69747a5472616365204953205448452042455354') - OR PATINDEX('%sp_BlitzTrace%',batch_text) <> 0 - OR PATINDEX('%sp_BlitzTrace%',sql_text) <> 0 - OPTION (RECOMPILE); - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying sql_batch_completed, rpc_completed, sql_statement_completed, sp_statement_completed...' - RAISERROR (@msg,0,1) WITH NOWAIT; - /* sql_batch_completed, rpc_completed */ - SELECT - event_time, - event_type, - batch_text, - [statement], - sql_text, - duration_micros, - cpu_micros, - physical_reads, - logical_reads, - writes, - row_count, - result, - query_hash, - query_plan_hash, - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type in (N'sql_batch_completed',N'rpc_completed','sql_statement_completed', 'sp_statement_completed') - ORDER BY 1; - - IF (SELECT TOP 1 event_time FROM #sp_BlitzTraceEvents WHERE event_type = N'degree_of_parallelism') - IS NOT NULL - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying parallelism and memory grant...' - RAISERROR (@msg,0,1) WITH NOWAIT; - /* parallelism and memory grant */ - SELECT - event_time, - event_type, - sql_text, - dop_statement_type, - dop, - workspace_memory_grant_kb, - query_hash, - query_plan_hash, - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type = 'degree_of_parallelism' - and query_hash <> '0' - ORDER BY 1; - END - - IF (SELECT TOP 1 event_time FROM #sp_BlitzTraceEvents WHERE event_type = N'object_created') - IS NOT NULL - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying object_created ...' - RAISERROR (@msg,0,1) WITH NOWAIT; - /* object_created */ - SELECT - event_time, - event_type, - sql_text, - [object_id], - [object_name], - cpu_micros, - ddl_phase, - query_hash, - query_plan_hash, - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type = 'object_created' - ORDER BY 1; - END - - IF (SELECT TOP 1 event_time FROM #sp_BlitzTraceEvents WHERE event_type = N'sql_statement_recompile') - IS NOT NULL - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying sql_statement_recompile ...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - /* sql_statement_recompile */ - SELECT - event_time, - event_type, - sql_text, - recompile_cause, - query_hash, - query_plan_hash, - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type = 'sql_statement_recompile' - ORDER BY 1; - END - - IF (SELECT TOP 1 event_time FROM #sp_BlitzTraceEvents WHERE event_type = N'sort_warning') - IS NOT NULL - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying sort_warning ...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - /* sql_statement_recompile */ - SELECT - event_time, - event_type, - sort_warning_type, - query_operation_node_id, - query_hash, - query_plan_hash, - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type = 'sort_warning' - ORDER BY 1; - END - - IF (SELECT TOP 1 event_time FROM #sp_BlitzTraceEvents WHERE event_type = N'query_post_execution_showplan') - IS NOT NULL - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Querying actual execution plans ...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - /* sql_statement_recompile */ - SELECT - event_time, - estimated_cost, - [object_name], - sql_text, - [showplan_xml] as [hope_this_isn't_production], - event_data - FROM #sp_BlitzTraceEvents - WHERE event_type = 'query_post_execution_showplan' - ORDER BY 1; - END - - END - ELSE /* We're in @Debug=1 mode */ - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- @Debug=1, not reading sp_BlitzTrace Extended Events session files;' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - END - - IF @Action = 'stop' - BEGIN - IF @tracerunning = 1 - BEGIN - IF @Debug = 0 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Stopping sp_BlitzTrace Extended Events session.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - ALTER EVENT SESSION sp_BlitzTrace ON SERVER STATE = STOP; - - SET @tracerunning = 0; - END - ELSE /* @Debug=1 */ - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- @Debug=1, sp_BlitzTrace Extended Events session is running but we are NOT stopping it;' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - END - ELSE - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- No running sp_BlitzTrace Extended Events session to stop.' - RAISERROR (@msg,16,1) WITH NOWAIT; - END - END - - - IF @Action = 'drop' - BEGIN - IF @traceexists = 1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Dropping sp_BlitzTrace Extended Events session.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - IF @Debug=0 - BEGIN - DROP EVENT SESSION sp_BlitzTrace ON SERVER; - - SET @traceexists=0; - END - ELSE /* @Debug=1 */ - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- @Debug=1, sp_BlitzTrace Extended Events session exists but we are NOT dropping it.' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - END - ELSE - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- No sp_BlitzTrace XEvents trace to drop.' - RAISERROR (@msg,16,1) WITH NOWAIT; - END - END - - RAISERROR (@nl,0,1) WITH NOWAIT; - RAISERROR (N'********************READ ME!********************',0,1) WITH NOWAIT; - - - IF @traceexists = 1 and @tracerunning = 0 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Extended Events session sp_BlitzTrace exists, but is stopped.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- To drop sp_BlitzTrace, run: exec dbo.sp_BlitzTrace @Action=''drop'';' - RAISERROR (@msg,0,1) WITH NOWAIT; - - END - ELSE IF @traceexists = 1 and @tracerunning = 1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Extended Events session sp_BlitzTrace exists and is running' + - CASE WHEN @SessionId is not null - THEN N' for @SessionId=' + cast(@SessionId as NVARCHAR(5)) - ELSE N'' - END - RAISERROR (@msg,0,1) WITH NOWAIT; - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Don''t leave the sp_BlitzTrace session running for long periods!' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- To stop sp_BlitzTrace, run: exec dbo.sp_BlitzTrace @Action=''stop'';' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - ELSE IF @traceexists = 0 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Extended Events session sp_BlitzTrace doesn''t exist, we''re all cleaned up.' - RAISERROR (@msg,0,1) WITH NOWAIT; - END - - RAISERROR (N'********************SEE YA********************',0,1) WITH NOWAIT; - - SET CONTEXT_INFO 0x; -END TRY -BEGIN CATCH - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Catching error ...' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SELECT - ERROR_NUMBER() AS ErrorNumber - ,ERROR_SEVERITY() AS ErrorSeverity - ,ERROR_STATE() AS ErrorState - ,ERROR_PROCEDURE() AS ErrorProcedure - ,ERROR_LINE() AS ErrorLine - ,ERROR_MESSAGE() AS ErrorMessage; - - /* Re-check trace status */ - SELECT - @traceexists = (CASE WHEN (s.name IS NULL) THEN 0 ELSE 1 END), - @tracerunning = (CASE WHEN (r.create_time IS NULL) THEN 0 ELSE 1 END) - FROM sys.server_event_sessions AS s - LEFT OUTER JOIN sys.dm_xe_sessions AS r ON r.name = s.name - WHERE s.name='sp_BlitzTrace' - - - IF @Action='start' and @traceexists=1 - BEGIN - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- An error occurred starting the trace. Cleaning it up.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - DROP EVENT SESSION sp_BlitzTrace ON SERVER; - END - - SET @msg= CONVERT(NVARCHAR(30), GETDATE(), 126) + N'- Resetting context and we''re outta here.' - RAISERROR (@msg,0,1) WITH NOWAIT; - - SET CONTEXT_INFO 0x; - -END CATCH - -GO diff --git a/Deprecated/sp_Blitz_SQL_Server_2005.sql b/Deprecated/sp_Blitz_SQL_Server_2005.sql new file mode 100755 index 000000000..81e3292db --- /dev/null +++ b/Deprecated/sp_Blitz_SQL_Server_2005.sql @@ -0,0 +1,5207 @@ +USE [master]; +GO + +IF EXISTS(SELECT * FROM sys.databases WHERE compatibility_level < 90 AND name = 'master') + RAISERROR ('sp_Blitz cannot be installed when master database is still in 2000 compatibility mode. For information: http://BrentOzar.com/blitz/', 20,1) WITH LOG, NOWAIT; +GO + +IF OBJECT_ID('dbo.sp_Blitz') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0;') +GO + +ALTER PROCEDURE [dbo].[sp_Blitz] + @CheckUserDatabaseObjects TINYINT = 1 , + @CheckProcedureCache TINYINT = 0 , + @OutputType VARCHAR(20) = 'TABLE' , + @OutputProcedureCache TINYINT = 0 , + @CheckProcedureCacheFilter VARCHAR(10) = NULL , + @CheckServerInfo TINYINT = 0 , + @SkipChecksServer NVARCHAR(256) = NULL , + @SkipChecksDatabase NVARCHAR(256) = NULL , + @SkipChecksSchema NVARCHAR(256) = NULL , + @SkipChecksTable NVARCHAR(256) = NULL , + @IgnorePrioritiesBelow INT = NULL , + @IgnorePrioritiesAbove INT = NULL , + @OutputDatabaseName NVARCHAR(128) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputXMLasNVARCHAR TINYINT = 0 , + @EmailRecipients VARCHAR(MAX) = NULL , + @EmailProfile sysname = NULL , + @SummaryMode TINYINT = 0 , + @Help TINYINT = 0 , + @Version INT = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT +AS + SET NOCOUNT ON; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @Version = 42, @VersionDate = '20150907' + + IF @Help = 1 PRINT ' + /* + sp_Blitz (TM) v42 - September 7, 2015 + + (C) 2015, Brent Ozar Unlimited. + See http://BrentOzar.com/go/eula for the End User Licensing Agreement. + + To learn more, visit http://www.BrentOzar.com/blitz where you can download + new versions for free, watch training videos on how it works, get more info on + the findings, and more. + + To request a feature or change: http://support.brentozar.com/ + To contribute code: http://www.brentozar.com/contributing-code/ + + Known limitations of this version: + - No support for SQL Server 2000 or compatibility mode 80. + - If a database name has a question mark in it, some tests will fail. Gotta + love that unsupported sp_MSforeachdb. + - If you have offline databases, sp_Blitz fails the first time you run it, + but does work the second time. (Hoo, boy, this will be fun to debug.) + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) + + Changes in v42 - September 7, 2015 + - Added check 163 for SQL Server 2016 databases with Query Store disabled. + - Added a few ignorable waits. + - Do not say no-significant-waits-found if we detected poison waits. + - Stop people from trying to install it in SQL Server 2000 compat mode. + - Bug fixes. + + Changes in v41 - June 18, 2015 + - Added check 162 for CMEMTHREAD waits on servers with >= 8 logical + processors per NUMA node. + - Added check 159 for NUMA nodes reporting dangerously low memory in + sys.dm_os_nodes. + - Added check 161 for a high number of cached plans per KB 3026083. + - Fixed a bug in the SkipChecks routines. Reported by Kevin Collins. + - Backup-to-same-drive-as-databases check (93) now includes the number of + backups that were done so you can tell if it was a one-off problem, or if + all backups are going to the wrong place. + - Bug fixes and improvements. + + Changes in v40 - April 27, 2015 + - Added check 158 for 1MB growth sizes on databases over 10GB. Probably time + to up that growth size. Contributed by Henrik Staun Poulsen. + - Added check 160 for queries with more than 50 execution plans in cache, + an indicator that it is not parameterized properly. + - Fixed check 97 that said Data Center Edition was subject to CPU and + memory limits, which is not true. It is only subject to wallet limits. + Reported by Brad Nelson. + - Fixed checks 106, 150, 151 to be skipped when the default trace file has + disappeared. Coded by Steve Coles. + - Fixed check 1, the VERY FIRST CHECK IN THE SCRIPT, which had a bug when + catching databases that had never been backed up. Sure, there was a + workaround in the next statement, but Julie Citro spotted the bug and + made it right. First check, people. All of you who ever read this code, + Julie Citro is officially a better code reviewer than you. + - Skip backup checks on offline databases. + - For order and join hints, raised threshold to 1000 instead of 1. + + For prior changes, see: http://www.BrentOzar.com/blitz/changelog/ + + + Parameter explanations: + + @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. + @CheckServerInfo 1=show server info like CPUs, memory, virtualization + @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. + @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm + @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' + @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''SCHEMA''=version and field list | ''NONE'' = none + @IgnorePrioritiesBelow 100=ignore priorities below 100 + @IgnorePrioritiesAbove 100=ignore priorities above 100 + For the rest of the parameters, see http://www.brentozar.com/blitz/documentation for details. + + + */' + ELSE IF @OutputType = 'SCHEMA' + BEGIN + SELECT @Version AS Version, + FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT' + + END + ELSE /* IF @OutputType = 'SCHEMA' */ + BEGIN + + /* + We start by creating #BlitzResults. It's a temp table that will store all of + the results from our checks. Throughout the rest of this stored procedure, + we're running a series of checks looking for dangerous things inside the SQL + Server. When we find a problem, we insert rows into #BlitzResults. At the + end, we return these results to the end user. + + #BlitzResults has a CheckID field, but there's no Check table. As we do + checks, we insert data into this table, and we manually put in the CheckID. + We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can + download that from http://www.BrentOzar.com/blitz/documentation/ - you'll + see why it can help shortly. + */ + DECLARE @StringToExecute NVARCHAR(4000) + ,@curr_tracefilename NVARCHAR(500) + ,@base_tracefilename NVARCHAR(500) + ,@indx int + ,@query_result_separator CHAR(1) + ,@EmailSubject NVARCHAR(255) + ,@EmailBody NVARCHAR(MAX) + ,@EmailAttachmentFilename NVARCHAR(255) + ,@ProductVersion NVARCHAR(128) + ,@ProductVersionMajor DECIMAL(10,2) + ,@ProductVersionMinor DECIMAL(10,2) + ,@CurrentName NVARCHAR(128) + ,@CurrentDefaultValue NVARCHAR(200) + ,@CurrentCheckID INT + ,@CurrentPriority INT + ,@CurrentFinding VARCHAR(200) + ,@CurrentURL VARCHAR(200) + ,@CurrentDetails NVARCHAR(4000) + ,@MSSinceStartup DECIMAL(38,0) + ,@CPUMSsinceStartup DECIMAL(38,0); + + IF OBJECT_ID('tempdb..#BlitzResults') IS NOT NULL + DROP TABLE #BlitzResults; + CREATE TABLE #BlitzResults + ( + ID INT IDENTITY(1, 1) , + CheckID INT , + DatabaseName NVARCHAR(128) , + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL + ); + + /* + You can build your own table with a list of checks to skip. For example, you + might have some databases that you don't care about, or some checks you don't + want to run. Then, when you run sp_Blitz, you can specify these parameters: + @SkipChecksDatabase = 'DBAtools', + @SkipChecksSchema = 'dbo', + @SkipChecksTable = 'BlitzChecksToSkip' + Pass in the database, schema, and table that contains the list of checks you + want to skip. This part of the code checks those parameters, gets the list, + and then saves those in a temp table. As we run each check, we'll see if we + need to skip it. + + Really anal-retentive users will note that the @SkipChecksServer parameter is + not used. YET. We added that parameter in so that we could avoid changing the + stored proc's surface area (interface) later. + */ + IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL + DROP TABLE #SkipChecks; + CREATE TABLE #SkipChecks + ( + DatabaseName NVARCHAR(128) , + CheckID INT , + ServerName NVARCHAR(128) + ); + CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); + + IF @SkipChecksTable IS NOT NULL + AND @SkipChecksSchema IS NOT NULL + AND @SkipChecksDatabase IS NOT NULL + BEGIN + SET @StringToExecute = 'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) + SELECT DISTINCT DatabaseName, CheckID, ServerName + FROM ' + QUOTENAME(@SkipChecksDatabase) + '.' + QUOTENAME(@SkipChecksSchema) + '.' + QUOTENAME(@SkipChecksTable) + + ' WHERE ServerName IS NULL OR ServerName = SERVERPROPERTY(''ServerName'');' + EXEC(@StringToExecute) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 106 ) + AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 + BEGIN + select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; + set @curr_tracefilename = reverse(@curr_tracefilename); + select @indx = patindex('%\%', @curr_tracefilename) ; + set @curr_tracefilename = reverse(@curr_tracefilename) ; + set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '\log.trc' ; + END + + + /* + That's the end of the SkipChecks stuff. + The next several tables are used by various checks later. + */ + IF OBJECT_ID('tempdb..#ConfigurationDefaults') IS NOT NULL + DROP TABLE #ConfigurationDefaults; + CREATE TABLE #ConfigurationDefaults + ( + name NVARCHAR(128) , + DefaultValue BIGINT, + CheckID INT + ); + + IF OBJECT_ID('tempdb..#DatabaseDefaults') IS NOT NULL + DROP TABLE #DatabaseDefaults; + CREATE TABLE #DatabaseDefaults + ( + name NVARCHAR(128) , + DefaultValue NVARCHAR(200), + CheckID INT, + Priority INT, + Finding VARCHAR(200), + URL VARCHAR(200), + Details NVARCHAR(4000) + ); + + + + IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL + DROP TABLE #DBCCs; + CREATE TABLE #DBCCs + ( + ID INT IDENTITY(1, 1) + PRIMARY KEY , + ParentObject VARCHAR(255) , + Object VARCHAR(255) , + Field VARCHAR(255) , + Value VARCHAR(255) , + DbName NVARCHAR(128) NULL + ) + + + IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL + DROP TABLE #LogInfo2012; + CREATE TABLE #LogInfo2012 + ( + recoveryunitid INT , + FileID SMALLINT , + FileSize BIGINT , + StartOffset BIGINT , + FSeqNo BIGINT , + [Status] TINYINT , + Parity TINYINT , + CreateLSN NUMERIC(38) + ); + + IF OBJECT_ID('tempdb..#LogInfo') IS NOT NULL + DROP TABLE #LogInfo; + CREATE TABLE #LogInfo + ( + FileID SMALLINT , + FileSize BIGINT , + StartOffset BIGINT , + FSeqNo BIGINT , + [Status] TINYINT , + Parity TINYINT , + CreateLSN NUMERIC(38) + ); + + IF OBJECT_ID('tempdb..#partdb') IS NOT NULL + DROP TABLE #partdb; + CREATE TABLE #partdb + ( + dbname NVARCHAR(128) , + objectname NVARCHAR(200) , + type_desc NVARCHAR(128) + ) + + IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL + DROP TABLE #TraceStatus; + CREATE TABLE #TraceStatus + ( + TraceFlag VARCHAR(10) , + status BIT , + Global BIT , + Session BIT + ); + + IF OBJECT_ID('tempdb..#driveInfo') IS NOT NULL + DROP TABLE #driveInfo; + CREATE TABLE #driveInfo + ( + drive NVARCHAR , + SIZE DECIMAL(18, 2) + ) + + + IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL + DROP TABLE #dm_exec_query_stats; + CREATE TABLE #dm_exec_query_stats + ( + [id] [int] NOT NULL + IDENTITY(1, 1) , + [sql_handle] [varbinary](64) NOT NULL , + [statement_start_offset] [int] NOT NULL , + [statement_end_offset] [int] NOT NULL , + [plan_generation_num] [bigint] NOT NULL , + [plan_handle] [varbinary](64) NOT NULL , + [creation_time] [datetime] NOT NULL , + [last_execution_time] [datetime] NOT NULL , + [execution_count] [bigint] NOT NULL , + [total_worker_time] [bigint] NOT NULL , + [last_worker_time] [bigint] NOT NULL , + [min_worker_time] [bigint] NOT NULL , + [max_worker_time] [bigint] NOT NULL , + [total_physical_reads] [bigint] NOT NULL , + [last_physical_reads] [bigint] NOT NULL , + [min_physical_reads] [bigint] NOT NULL , + [max_physical_reads] [bigint] NOT NULL , + [total_logical_writes] [bigint] NOT NULL , + [last_logical_writes] [bigint] NOT NULL , + [min_logical_writes] [bigint] NOT NULL , + [max_logical_writes] [bigint] NOT NULL , + [total_logical_reads] [bigint] NOT NULL , + [last_logical_reads] [bigint] NOT NULL , + [min_logical_reads] [bigint] NOT NULL , + [max_logical_reads] [bigint] NOT NULL , + [total_clr_time] [bigint] NOT NULL , + [last_clr_time] [bigint] NOT NULL , + [min_clr_time] [bigint] NOT NULL , + [max_clr_time] [bigint] NOT NULL , + [total_elapsed_time] [bigint] NOT NULL , + [last_elapsed_time] [bigint] NOT NULL , + [min_elapsed_time] [bigint] NOT NULL , + [max_elapsed_time] [bigint] NOT NULL , + [query_hash] [binary](8) NULL , + [query_plan_hash] [binary](8) NULL , + [query_plan] [xml] NULL , + [query_plan_filtered] [nvarchar](MAX) NULL , + [text] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS + NULL , + [text_filtered] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS + NULL + ) + + /* Used for the default trace checks. */ + DECLARE @TracePath NVARCHAR(256); + SELECT @TracePath=CAST(value as NVARCHAR(256)) + FROM sys.fn_trace_getinfo(1) + WHERE traceid=1 AND property=2; + + SELECT @MSSinceStartup = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) + FROM sys.databases + WHERE name='tempdb'; + + SET @MSSinceStartup = @MSSinceStartup * 60000; + + SELECT @CPUMSsinceStartup = @MSSinceStartup * cpu_count + FROM sys.dm_os_sys_info; + + + /* If we're outputting CSV, don't bother checking the plan cache because we cannot export plans. */ + IF @OutputType = 'CSV' + SET @CheckProcedureCache = 0; + + /* Sanitize our inputs */ + SELECT + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName) + + /* Get the major and minor build numbers */ + SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); + SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), + @ProductVersionMinor = PARSENAME(CONVERT(varchar(32), @ProductVersion), 2) + + + /* + Whew! we're finally done with the setup, and we can start doing checks. + First, let's make sure we're actually supposed to do checks on this server. + The user could have passed in a SkipChecks table that specified to skip ALL + checks on this server, so let's check for that: + */ + IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID IS NULL ) ) + OR ( @SkipChecksTable IS NULL ) + ) + BEGIN + + /* + Our very first check! We'll put more comments in this one just to + explain exactly how it works. First, we check to see if we're + supposed to skip CheckID 1 (that's the check we're working on.) + */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 1 ) + BEGIN + + /* + Below, we check master.sys.databases looking for databases + that haven't had a backup in the last week. If we find any, + we insert them into #BlitzResults, the temp table that + tracks our server's problems. Note that if the check does + NOT find any problems, we don't save that. We're only + saving the problems, not the successful checks. + */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 1 AS CheckID , + d.[name] AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backups Not Performed Recently' AS Finding , + 'http://BrentOzar.com/go/nobak' AS URL , + 'Database ' + d.Name + ' last backed up: ' + + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details + FROM master.sys.databases d + LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'D' + AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ + WHERE d.database_id <> 2 /* Bonus points if you know what that means */ + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL ) + /* + The above NOT IN filters out the databases we're not supposed to check. + */ + GROUP BY d.name + HAVING MAX(b.backup_finish_date) <= DATEADD(dd, + -7, GETDATE()) + OR MAX(b.backup_finish_date) IS NULL; + /* + And there you have it. The rest of this stored procedure works the same + way: it asks: + - Should I skip this check? + - If not, do I find problems? + - Insert the results into #BlitzResults + */ + + END + + /* + And that's the end of CheckID #1. + + CheckID #2 is a little simpler because it only involves one query, and it's + more typical for queries that people contribute. But keep reading, because + the next check gets more complex again. + */ + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 2 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 2 AS CheckID , + d.name AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Full Recovery Mode w/o Log Backups' AS Finding , + 'http://BrentOzar.com/go/biglogs' AS URL , + ( 'Database ' + ( d.Name COLLATE database_default ) + + ' is in ' + d.recovery_model_desc + + ' recovery mode but has not had a log backup in the last week.' ) AS Details + FROM master.sys.databases d + WHERE d.recovery_model IN ( 1, 2 ) + AND d.database_id NOT IN ( 2, 3 ) + AND d.source_database_id IS NULL + AND d.state <> 1 /* Not currently restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL ) + AND NOT EXISTS ( SELECT * + FROM msdb.dbo.backupset b + WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd, + -7, GETDATE()) ); + END + + + /* + Next up, we've got CheckID 8. (These don't have to go in order.) This one + won't work on SQL Server 2005 because it relies on a new DMV that didn't + exist prior to SQL Server 2008. This means we have to check the SQL Server + version first, then build a dynamic string with the query we want to run: + */ + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 8 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, Priority, + FindingsGroup, + Finding, URL, + Details) + SELECT 8 AS CheckID, + 150 AS Priority, + ''Security'' AS FindingsGroup, + ''Server Audits Running'' AS Finding, + ''http://BrentOzar.com/go/audits'' AS URL, + (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status' + EXECUTE(@StringToExecute) + END; + END + + /* + But what if you need to run a query in every individual database? + Check out CheckID 99 below. Yes, it uses sp_MSforeachdb, and no, + we're not happy about that. sp_MSforeachdb is known to have a lot + of issues, like skipping databases sometimes. However, this is the + only built-in option that we have. If you're writing your own code + for database maintenance, consider Aaron Bertrand's alternative: + http://www.mssqltips.com/sqlservertip/2201/making-a-more-reliable-and-flexible-spmsforeachdb/ + We don't include that as part of sp_Blitz, of course, because + copying and distributing copyrighted code from others without their + written permission isn't a good idea. + */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 99 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; 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'', ''http://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 + databases. We're not checking #SkipChecks here for each database to + see if we should run the check in this database. That means we may + still run a skipped check if it involves sp_MSforeachdb. We just + don't output those results in the last step. + + And that's the basic idea! You can read through the rest of the + checks if you like - some more exciting stuff happens closer to the + end of the stored proc, where we start doing things like checking + the plan cache, but those aren't as cleanly commented. + + If you'd like to contribute your own check, use one of the check + formats shown above and email it to Help@BrentOzar.com. You don't + have to pick a CheckID or a link - we'll take care of that when we + test and publish the code. Thanks! + */ + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 93 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 93 AS CheckID , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backing Up to Same Drive Where Databases Reside' AS Finding , + 'http://BrentOzar.com/go/backup' AS URL , + CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' + + UPPER(LEFT(bmf.physical_device_name, 3)) + + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details + FROM msdb.dbo.backupmediafamily AS bmf + INNER JOIN msdb.dbo.backupset AS bs ON bmf.media_set_id = bs.media_set_id + AND bs.backup_start_date >= ( DATEADD(dd, + -14, GETDATE()) ) + WHERE UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( + SELECT DISTINCT + UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) + FROM sys.master_files AS mf ) + GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 119 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_database_encryption_keys' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, DatabaseName, URL, Details) + SELECT 119 AS CheckID, + 1 AS Priority, + ''Backup'' AS FindingsGroup, + ''TDE Certificate Not Backed Up Recently'' AS Finding, + db_name(dek.database_id) AS DatabaseName, + ''http://BrentOzar.com/go/tde'' AS URL, + ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details + FROM sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint + WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE())'; + EXECUTE(@StringToExecute); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 3 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 3 AS CheckID , + 'msdb' , + 200 AS Priority , + 'Backup' AS FindingsGroup , + 'MSDB Backup History Not Purged' AS Finding , + 'http://BrentOzar.com/go/history' AS URL , + ( 'Database backup history retained back to ' + + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details + FROM msdb.dbo.backupset bs + WHERE bs.backup_start_date <= DATEADD(dd, -60, + GETDATE()) + ORDER BY backup_set_id ASC; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 4 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 4 AS CheckID , + 10 AS Priority , + 'Security' AS FindingsGroup , + 'Sysadmins' AS Finding , + 'http://BrentOzar.com/go/sa' AS URL , + ( 'Login [' + l.name + + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details + FROM master.sys.syslogins l + WHERE l.sysadmin = 1 + AND l.name <> SUSER_SNAME(0x01) + AND l.denylogin = 0; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 5 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 5 AS CheckID , + 10 AS Priority , + 'Security' AS FindingsGroup , + 'Security Admins' AS Finding , + 'http://BrentOzar.com/go/sa' AS URL , + ( 'Login [' + l.name + + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details + FROM master.sys.syslogins l + WHERE l.securityadmin = 1 + AND l.name <> SUSER_SNAME(0x01) + AND l.denylogin = 0; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 104 ) + BEGIN + INSERT INTO #BlitzResults + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] + ) + SELECT 104 AS [CheckID] , + 10 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Login Can Control Server' AS [Finding] , + 'http://BrentOzar.com/go/sa' AS [URL] , + 'Login [' + pri.[name] + + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] + FROM sys.server_principals AS pri + WHERE pri.[principal_id] IN ( + SELECT p.[grantee_principal_id] + FROM sys.server_permissions AS p + WHERE p.[state] IN ( 'G', 'W' ) + AND p.[class] = 100 + AND p.[type] = 'CL' ) + AND pri.[name] NOT LIKE '##%##' + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 6 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 6 AS CheckID , + 200 AS Priority , + 'Security' AS FindingsGroup , + 'Jobs Owned By Users' AS Finding , + 'http://BrentOzar.com/go/owners' AS URL , + ( 'Job [' + j.name + '] is owned by [' + + SUSER_SNAME(j.owner_sid) + + '] - 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); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 7 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 7 AS CheckID , + 10 AS Priority , + 'Security' AS FindingsGroup , + 'Stored Procedure Runs at Startup' AS Finding , + 'http://BrentOzar.com/go/startup' AS URL , + ( 'Stored procedure [master].[' + + r.SPECIFIC_SCHEMA + '].[' + + r.SPECIFIC_NAME + + '] runs automatically when SQL Server starts up. Make sure you know exactly what this stored procedure is doing, because it could pose a security risk.' ) AS Details + FROM master.INFORMATION_SCHEMA.ROUTINES r + WHERE OBJECTPROPERTY(OBJECT_ID(ROUTINE_NAME), + 'ExecIsStartup') = 1; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 9 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 9 AS CheckID, + 200 AS Priority, + ''Surface Area'' AS FindingsGroup, + ''Endpoints Configured'' AS Finding, + ''http://BrentOzar.com/go/endpoints/'' AS URL, + (''SQL Server endpoints are configured. These can be used for database mirroring or Service Broker, but if you do not need them, avoid leaving them enabled. Endpoint name: '' + [name]) AS Details FROM sys.endpoints WHERE type <> 2' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 10 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 10 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Resource Governor Enabled'' AS Finding, + ''http://BrentOzar.com/go/rg'' AS URL, + (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 11 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 11 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Server Triggers Enabled'' AS Finding, + ''http://BrentOzar.com/go/logontriggers/'' AS URL, + (''Server Trigger ['' + [name] ++ ''] is enabled, so it runs every time someone logs in. 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' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 12 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 12 AS CheckID , + [name] AS DatabaseName , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Close Enabled' AS Finding , + 'http://BrentOzar.com/go/autoclose' AS URL , + ( 'Database [' + [name] + + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details + FROM sys.databases + WHERE is_auto_close_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 13 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 13 AS CheckID , + [name] AS DatabaseName , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Shrink Enabled' AS Finding , + 'http://BrentOzar.com/go/autoshrink' AS URL , + ( 'Database [' + [name] + + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details + FROM sys.databases + WHERE is_auto_shrink_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 14 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 14 AS CheckID, + [name] as DatabaseName, + 50 AS Priority, + ''Reliability'' AS FindingsGroup, + ''Page Verification Not Optimal'' AS Finding, + ''http://BrentOzar.com/go/torn'' AS URL, + (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details + FROM sys.databases + WHERE page_verify_option < 2 + AND name <> ''tempdb'' + and name not in (select distinct DatabaseName from #SkipChecks)' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 15 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 15 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Create Stats Disabled' AS Finding , + 'http://BrentOzar.com/go/acs' AS URL , + ( 'Database [' + [name] + + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details + FROM sys.databases + WHERE is_auto_create_stats_on = 0 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 16 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 16 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Update Stats Disabled' AS Finding , + 'http://BrentOzar.com/go/aus' AS URL , + ( 'Database [' + [name] + + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details + FROM sys.databases + WHERE is_auto_update_stats_on = 0 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 17 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 17 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Stats Updated Asynchronously' AS Finding , + 'http://BrentOzar.com/go/asyncstats' AS URL , + ( 'Database [' + [name] + + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details + FROM sys.databases + WHERE is_auto_update_stats_async_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 18 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 18 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Forced Parameterization On' AS Finding , + 'http://BrentOzar.com/go/forced' AS URL , + ( 'Database [' + [name] + + '] has forced parameterization enabled. SQL Server will aggressively reuse query execution plans even if the applications do not parameterize their queries. This can be a performance booster with some programming languages, or it may use universally bad execution plans when better alternatives are available for certain parameters.' ) AS Details + FROM sys.databases + WHERE is_parameterization_forced = 1 + AND name NOT IN ( SELECT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 19 ) + BEGIN + /* Method 1: Check sys.databases parameters */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + + SELECT 19 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Replication In Use' AS Finding , + 'http://BrentOzar.com/go/repl' AS URL , + ( 'Database [' + [name] + + '] is a replication publisher, subscriber, or distributor.' ) AS Details + FROM sys.databases + WHERE name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + AND is_published = 1 + OR is_subscribed = 1 + OR is_merge_published = 1 + OR is_distributor = 1; + + /* Method B: check subscribers for MSreplication_objects tables */ + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 19, + db_name(), + 200, + ''Informational'', + ''Replication In Use'', + ''http://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'''; + + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 20 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 20 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Informational' AS FindingsGroup , + 'Date Correlation On' AS Finding , + 'http://BrentOzar.com/go/corr' AS URL , + ( 'Database [' + [name] + + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details + FROM sys.databases + WHERE is_date_correlation_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 21 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 21 AS CheckID, + [name] as DatabaseName, + 20 AS Priority, + ''Encryption'' AS FindingsGroup, + ''Database Encrypted'' AS Finding, + ''http://BrentOzar.com/go/tde'' AS URL, + (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details + FROM sys.databases + WHERE is_encrypted = 1 + and name not in (select distinct DatabaseName from #SkipChecks)' + EXECUTE(@StringToExecute) + END; + END + + /* + Believe it or not, SQL Server doesn't track the default values + for sp_configure options! We'll make our own list here. + */ + 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 ( 'Agent XPs', 0, 1006 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'allow updates', 0, 1007 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'awe enabled', 0, 1008 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'blocked process threshold', 0, 1009 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'c2 audit mode', 0, 1010 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'clr enabled', 0, 1011 ); + 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, 1015 ); + 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 ( '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 ); + 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 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'affinity64 mask', 0, 1066 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'affinity64 I/O mask', 0, 1067 ); + INSERT INTO #ConfigurationDefaults + VALUES ( 'contained database authentication', 0, 1068 ); + /* SQL Server 2012 also 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 + ELSE + BEGIN + INSERT INTO #ConfigurationDefaults + VALUES ( 'remote login timeout (s)', 10, 1070 ); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 22 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT cd.CheckID , + 200 AS Priority , + 'Non-Default Server Config' AS FindingsGroup , + cr.name AS Finding , + 'http://BrentOzar.com/go/conf' AS URL , + ( 'This sp_configure option has been changed. Its default value is ' + + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), + '(unknown)') + + ' and it has been set to ' + + CAST(cr.value_in_use AS VARCHAR(100)) + + '.' ) AS Details + FROM sys.configurations cr + INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name + LEFT OUTER JOIN #ConfigurationDefaults cdUsed ON cdUsed.name = cr.name + AND cdUsed.DefaultValue = cr.value_in_use + WHERE cdUsed.name IS NULL; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 24 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 24 AS CheckID , + DB_NAME(database_id) AS DatabaseName , + 20 AS Priority , + 'Reliability' AS FindingsGroup , + 'System Database on C Drive' AS Finding , + 'http://BrentOzar.com/go/cdrive' AS URL , + ( 'The ' + DB_NAME(database_id) + + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) IN ( 'master', + 'model', 'msdb' ); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 25 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 25 AS CheckID , + 'tempdb' , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'TempDB on C Drive' AS Finding , + 'http://BrentOzar.com/go/cdrive' AS URL , + CASE WHEN growth > 0 + THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) + ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) + END AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) = 'tempdb'; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 26 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 26 AS CheckID , + DB_NAME(database_id) AS DatabaseName , + 20 AS Priority , + 'Reliability' AS FindingsGroup , + 'User Databases on C Drive' AS Finding , + 'http://BrentOzar.com/go/cdrive' AS URL , + ( 'The ' + DB_NAME(database_id) + + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) NOT IN ( 'master', + 'model', 'msdb', + 'tempdb' ) + AND DB_NAME(database_id) NOT IN ( + SELECT DISTINCT + DatabaseName + FROM #SkipChecks ) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 27 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 27 AS CheckID , + 'master' AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the Master Database' AS Finding , + 'http://BrentOzar.com/go/mastuser' AS URL , + ( 'The ' + name + + ' table in the master database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. 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; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 28 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 28 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the MSDB Database' AS Finding , + 'http://BrentOzar.com/go/msdbuser' AS URL , + ( 'The ' + name + + ' table in the msdb database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. Tables in the msdb database may not be restored in the event of a disaster.' ) AS Details + FROM msdb.sys.tables + WHERE is_ms_shipped = 0; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 29 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 29 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the Model Database' AS Finding , + 'http://BrentOzar.com/go/model' AS URL , + ( 'The ' + name + + ' table in the model database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. Tables in the model database are automatically copied into all new databases.' ) AS Details + FROM model.sys.tables + WHERE is_ms_shipped = 0; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 30 ) + BEGIN + IF ( SELECT COUNT(*) + FROM msdb.dbo.sysalerts + WHERE severity BETWEEN 19 AND 25 + ) < 7 + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 30 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Not All Alerts Configured' AS Finding , + 'http://BrentOzar.com/go/alert' AS URL , + ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; + END + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 59 ) + BEGIN + IF EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE enabled = 1 + AND COALESCE(has_notification, 0) = 0 + AND (job_id IS NULL OR job_id = 0x)) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 59 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Alerts Configured without Follow Up' AS Finding , + 'http://BrentOzar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 96 ) + BEGIN + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE message_id IN ( 823, 824, 825 ) ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 96 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'No Alerts for Corruption' AS Finding , + 'http://BrentOzar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 61 ) + BEGIN + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE severity BETWEEN 19 AND 25 ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 61 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'No Alerts for Sev 19-25' AS Finding , + 'http://BrentOzar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; + END + + --check for disabled alerts + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 98 ) + BEGIN + IF EXISTS ( SELECT name + FROM msdb.dbo.sysalerts + WHERE enabled = 0 ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 98 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Alerts Disabled' AS Finding , + 'http://www.BrentOzar.com/go/alerts/' AS URL , + ( 'The following Alert is disabled, please review and enable if desired: ' + + name ) AS Details + FROM msdb.dbo.sysalerts + WHERE enabled = 0 + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 31 ) + BEGIN + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysoperators + WHERE enabled = 1 ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 31 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'No Operators Configured/Enabled' AS Finding , + 'http://BrentOzar.com/go/op' AS URL , + ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 33 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 33, + db_name(), + 200, + ''Licensing'', + ''Enterprise Edition Features In Use'', + ''http://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.'') + FROM [?].sys.dm_db_persisted_sku_features'; + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 34 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_db_mirroring_auto_page_repair' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 34 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''http://BrentOzar.com/go/repair'' AS URL , + ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details + FROM (SELECT rp2.database_id, rp2.modification_time + FROM sys.dm_db_mirroring_auto_page_repair rp2 + WHERE rp2.[database_id] not in ( + SELECT db2.[database_id] + FROM sys.databases as db2 + WHERE db2.[state] = 1 + ) ) as rp + INNER JOIN master.sys.databases db ON rp.database_id = db.database_id + WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) ;' + EXECUTE(@StringToExecute) + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 89 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_hadr_auto_page_repair' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 89 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''http://BrentOzar.com/go/repair'' AS URL , + ( ''AlwaysOn has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details + FROM sys.dm_hadr_auto_page_repair rp + INNER JOIN master.sys.databases db ON rp.database_id = db.database_id + WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) ;' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 90 ) + BEGIN + IF EXISTS ( SELECT * + FROM msdb.sys.all_objects + WHERE name = 'suspect_pages' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 90 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''http://BrentOzar.com/go/repair'' AS URL , + ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details + FROM msdb.dbo.suspect_pages sp + INNER JOIN master.sys.databases db ON sp.database_id = db.database_id + WHERE sp.last_update_date >= DATEADD(dd, -30, GETDATE()) ;' + EXECUTE(@StringToExecute) + END; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 36 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 36 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Slow Storage Reads on Drive ' + + UPPER(LEFT(mf.physical_name, 1)) AS Finding , + 'http://BrentOzar.com/go/slow' AS URL , + 'Reads are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details + FROM sys.dm_io_virtual_file_stats(NULL, NULL) + AS fs + INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id + AND fs.[file_id] = mf.[file_id] + WHERE ( io_stall_read_ms / ( 1.0 + num_of_reads ) ) > 100; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 37 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 37 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Slow Storage Writes on Drive ' + + UPPER(LEFT(mf.physical_name, 1)) AS Finding , + 'http://BrentOzar.com/go/slow' AS URL , + 'Writes are averaging longer than 20ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details + FROM sys.dm_io_virtual_file_stats(NULL, NULL) + AS fs + INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id + AND fs.[file_id] = mf.[file_id] + WHERE ( io_stall_write_ms / ( 1.0 + + num_of_writes ) ) > 20; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 40 ) + BEGIN + IF ( SELECT COUNT(*) + FROM tempdb.sys.database_files + WHERE type_desc = 'ROWS' + ) = 1 + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( 40 , + 'tempdb' , + 100 , + 'Performance' , + 'TempDB Only Has 1 Data File' , + 'http://BrentOzar.com/go/tempdb' , + 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' + ); + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 41 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'use [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 41, + ''?'', + 100, + ''Performance'', + ''Multiple Log Files on One Drive'', + ''http://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 ''?'' <> ''[tempdb]'' + GROUP BY LEFT(physical_name, 1) + HAVING COUNT(*) > 1'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 42 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'use [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 42, + ''?'', + 100, + ''Performance'', + ''Uneven File Growth Settings in One Filegroup'', + ''http://BrentOzar.com/go/grow'', + (''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'' + GROUP BY data_space_id + HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 44 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 44 AS CheckID , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Queries Forcing Order Hints' AS Finding , + 'http://BrentOzar.com/go/hints' AS URL , + CAST(occurrence AS VARCHAR(10)) + + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details + FROM sys.dm_exec_query_optimizer_info + WHERE counter = 'order hint' + AND occurrence > 1000 + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 45 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 45 AS CheckID , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Queries Forcing Join Hints' AS Finding , + 'http://BrentOzar.com/go/hints' AS URL , + CAST(occurrence AS VARCHAR(10)) + + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details + FROM sys.dm_exec_query_optimizer_info + WHERE counter = 'join hint' + AND occurrence > 1000 + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 49 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 49 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Linked Server Configured' AS Finding , + 'http://BrentOzar.com/go/link' AS URL , + +CASE WHEN l.remote_name = 'sa' + THEN s.data_source + + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' + ELSE s.data_source + + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' + END AS Details + FROM sys.servers s + INNER JOIN sys.linked_logins l ON s.server_id = l.server_id + WHERE s.is_linked = 1 + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 50 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 50 AS CheckID , + 100 AS Priority , + ''Performance'' AS FindingsGroup , + ''Max Memory Set Too High'' AS Finding , + ''http://BrentOzar.com/go/max'' AS URL , + ''SQL Server max memory is set to '' + + CAST(c.value_in_use AS VARCHAR(20)) + + '' megabytes, but the server only has '' + + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + + '' megabytes. SQL Server may drain the system dry of memory, and under certain conditions, this can cause Windows to swap to disk.'' AS Details + FROM sys.dm_os_sys_memory m + INNER JOIN sys.configurations c ON c.name = ''max server memory (MB)'' + WHERE CAST(m.total_physical_memory_kb AS BIGINT) < ( CAST(c.value_in_use AS BIGINT) * 1024 )' + EXECUTE(@StringToExecute) + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 51 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 51 AS CheckID , + 1 AS Priority , + ''Performance'' AS FindingsGroup , + ''Memory Dangerously Low'' AS Finding , + ''http://BrentOzar.com/go/max'' AS URL , + ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details + FROM sys.dm_os_sys_memory m + WHERE CAST(m.available_physical_memory_kb AS BIGINT) < 262144' + EXECUTE(@StringToExecute) + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 159 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 159 AS CheckID , + 1 AS Priority , + ''Performance'' AS FindingsGroup , + ''Memory Dangerously Low in NUMA Nodes'' AS Finding , + ''http://BrentOzar.com/go/max'' AS URL , + ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details + FROM sys.dm_os_nodes m + WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%''' + EXECUTE(@StringToExecute) + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 53 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 53 AS CheckID , + 200 AS Priority , + 'High Availability' AS FindingsGroup , + 'Cluster Node' AS Finding , + 'http://BrentOzar.com/go/node' AS URL , + 'This is a node in a cluster.' AS Details + FROM sys.dm_os_cluster_nodes + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 55 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 55 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Security' AS FindingsGroup , + 'Database Owner <> SA' AS Finding , + 'http://BrentOzar.com/go/owndb' AS URL , + ( 'Database name: ' + [name] + ' ' + + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details + FROM sys.databases + WHERE SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01) + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL); + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 57 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 57 AS CheckID , + 10 AS Priority , + 'Security' AS FindingsGroup , + 'SQL Agent Job Runs at Startup' AS Finding , + 'http://BrentOzar.com/go/startup' AS URL , + ( 'Job [' + j.name + + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details + FROM msdb.dbo.sysschedules sched + JOIN msdb.dbo.sysjobschedules jsched ON sched.schedule_id = jsched.schedule_id + JOIN msdb.dbo.sysjobs j ON jsched.job_id = j.job_id + WHERE sched.freq_type = 64; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 82 ) + BEGIN + EXEC sp_MSforeachdb 'use [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, Details) + SELECT DISTINCT 82 AS CheckID, + ''?'' as DatabaseName, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''File growth set to percent'', + ''http://brentozar.com/go/percentgrowth'' AS URL, + ''The ['' + DB_NAME() + ''] database is using percent filegrowth settings. This can lead to out of control filegrowth.'' + FROM [?].sys.database_files + WHERE is_percent_growth = 1 '; + END + + /* addition by Henrik Staun Poulsen, Stovi Software */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 158 ) + BEGIN + EXEC sp_MSforeachdb 'use [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, Details) + SELECT DISTINCT 158 AS CheckID, + ''?'' as DatabaseName, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''File growth set to 1MB'', + ''http://brentozar.com/go/percentgrowth'' AS URL, + ''The ['' + DB_NAME() + ''] database is using 1MB filegrowth settings, but it has grown larger than 10GB. Time to up the growth amount.'' + FROM [?].sys.database_files + WHERE is_percent_growth = 0 and growth=128 and size > 1280000 '; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 97 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 97 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Unusual SQL Server Edition' AS Finding , + 'http://BrentOzar.com/go/workgroup' AS URL , + ( 'This server is using ' + + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + + ', which is capped at low amounts of CPU and memory.' ) AS Details + WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Data Center%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Business Intelligence%' + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 97 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 154 AS CheckID , + 10 AS Priority , + 'Performance' AS FindingsGroup , + '32-bit SQL Server Installed' AS Finding , + 'http://BrentOzar.com/go/32bit' AS URL , + ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details + WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%' + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 62 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 62 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Performance' AS FindingsGroup , + 'Old Compatibility Level' AS Finding , + 'http://BrentOzar.com/go/compatlevel' AS URL , + ( 'Database ' + [name] + + ' is compatibility level ' + + CAST(compatibility_level AS VARCHAR(20)) + + ', which may cause unwanted results when trying to run queries that have newer T-SQL features.' ) AS Details + FROM sys.databases + WHERE name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + AND compatibility_level <> ( SELECT + compatibility_level + FROM + sys.databases + WHERE + [name] = 'model' + ) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 94 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 94 AS CheckID , + 50 AS [Priority] , + 'Reliability' AS FindingsGroup , + 'Agent Jobs Without Failure Emails' AS Finding , + 'http://BrentOzar.com/go/alerts' AS URL , + 'The job ' + [name] + + ' has not been set up to notify an operator if it fails.' AS Details + FROM msdb.[dbo].[sysjobs] j + INNER JOIN ( SELECT DISTINCT + [job_id] + FROM [msdb].[dbo].[sysjobschedules] + WHERE next_run_date > 0 + ) s ON j.job_id = s.job_id + WHERE j.enabled = 1 + AND j.notify_email_operator_id = 0 + AND j.notify_netsend_operator_id = 0 + AND j.notify_page_operator_id = 0 + AND j.category_id <> 100 /* Exclude SSRS category */ + END + + + IF EXISTS ( SELECT 1 + FROM sys.configurations + WHERE name = 'remote admin connections' + AND value_in_use = 0 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 100 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 100 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingGroup , + 'Remote DAC Disabled' AS Finding , + 'http://BrentOzar.com/go/dac' AS URL , + 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.' + END + + + IF EXISTS ( SELECT * + FROM sys.dm_os_schedulers + WHERE is_online = 0 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 101 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 101 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'CPU Schedulers Offline' AS Finding , + 'http://BrentOzar.com/go/schedulers' AS URL , + 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.' + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 110 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_os_memory_nodes') + BEGIN + SET @StringToExecute = 'IF EXISTS (SELECT * + FROM sys.dm_os_nodes n + INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id + WHERE n.node_state_desc = ''OFFLINE'') + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 110 AS CheckID , + 50 AS Priority , + ''Performance'' AS FindingGroup , + ''Memory Nodes Offline'' AS Finding , + ''http://BrentOzar.com/go/schedulers'' AS URL , + ''Due to affinity masking or licensing problems, some of the memory may not be available.'''; + EXECUTE(@StringToExecute); + END + + + IF EXISTS ( SELECT * + FROM sys.databases + WHERE state > 1 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 102 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 102 AS CheckID , + [name] , + 20 AS Priority , + 'Reliability' AS FindingGroup , + 'Unusual Database State: ' + [state_desc] AS Finding , + 'http://BrentOzar.com/go/repair' AS URL , + 'This database may not be online.' + FROM sys.databases + WHERE state > 1 + END + + IF EXISTS ( SELECT * + FROM master.sys.extended_procedures ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 105 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 105 AS CheckID , + 'master' , + 50 AS Priority , + 'Reliability' AS FindingGroup , + 'Extended Stored Procedures in Master' AS Finding , + 'http://BrentOzar.com/go/clr' AS URL , + 'The [' + name + + '] extended stored procedure is in the master database. CLR may be in use, and the master database now needs to be part of your backup/recovery planning.' + FROM master.sys.extended_procedures + END + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 107 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 107 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: THREADPOOL' AS Finding , + 'http://BrentOzar.com/go/poison' AS URL , + CAST(SUM([wait_time_ms]) AS VARCHAR(100)) + ' milliseconds of this wait have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type = 'THREADPOOL' + GROUP BY wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 108 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 108 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: RESOURCE_SEMAPHORE' AS Finding , + 'http://BrentOzar.com/go/poison' AS URL , + CAST(SUM([wait_time_ms]) AS VARCHAR(100)) + ' milliseconds of this wait have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type = 'RESOURCE_SEMAPHORE' + GROUP BY wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 109 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 109 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: RESOURCE_SEMAPHORE_QUERY_COMPILE' AS Finding , + 'http://BrentOzar.com/go/poison' AS URL , + CAST(SUM([wait_time_ms]) AS VARCHAR(100)) + ' milliseconds of this wait have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type = 'RESOURCE_SEMAPHORE_QUERY_COMPILE' + GROUP BY wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 121 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 121 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: Serializable Locking' AS Finding , + 'http://BrentOzar.com/go/serializable' AS URL , + CAST(SUM([wait_time_ms]) / 1000 AS VARCHAR(100)) + ' seconds of this wait have been recorded. Queries are forcing serial operation (one query at a time) with lock hints.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type LIKE '%LCK%R%' + GROUP BY wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + END + + + + + IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 162 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 162 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: CMEMTHREAD & NUMA' AS Finding , + 'http://BrentOzar.com/go/poison' AS URL , + CAST(SUM([wait_time_ms]) / 1000 AS VARCHAR(100)) + ' seconds of this wait have been recorded. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' + FROM sys.dm_os_nodes n + INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' + WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 + GROUP BY w.wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 111 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + DatabaseName , + URL , + Details + ) + SELECT 111 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingGroup , + 'Possibly Broken Log Shipping' AS Finding , + d.[name] , + 'http://BrentOzar.com/go/shipping' AS URL , + d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' + FROM [master].sys.databases d + INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id + AND dm.mirroring_role IS NULL + WHERE ( d.[state] = 1 + OR (d.[state] = 0 AND d.[is_in_standby] = 1) ) + AND NOT EXISTS(SELECT * FROM msdb.dbo.restorehistory rh + INNER JOIN msdb.dbo.backupset bs ON rh.backup_set_id = bs.backup_set_id + WHERE d.[name] COLLATE SQL_Latin1_General_CP1_CI_AS = rh.destination_database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND rh.restore_date >= DATEADD(dd, -2, GETDATE())) + + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 112 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'change_tracking_databases') + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 112 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Change Tracking Enabled'' AS Finding, + ''http://BrentOzar.com/go/tracking'' AS URL, + ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id'; + EXECUTE(@StringToExecute); + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 116 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 116 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingGroup , + 'Backup Compression Default Off' AS Finding , + 'http://BrentOzar.com/go/backup' AS URL , + '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.' + FROM sys.configurations + WHERE configuration_id = 1579 AND CAST(value_in_use AS INT) = 0 + + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 117 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_exec_query_resource_semaphores') + BEGIN + SET @StringToExecute = 'IF 0 < (SELECT SUM([forced_grant_count]) FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL) + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 117 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Pressure Affecting Queries'' AS Finding, + ''http://BrentOzar.com/go/grants'' AS URL, + CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' + FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL;' + EXECUTE(@StringToExecute); + END + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 124 ) + BEGIN + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 124, 100, 'Performance', 'Deadlocks Happening Daily', 'http://BrentOzar.com/go/deadlocks', + CAST(p.cntr_value AS NVARCHAR(100)) + ' deadlocks have been recorded since startup.' AS Details + FROM sys.dm_os_performance_counters p + INNER JOIN sys.databases d ON d.name = 'tempdb' + WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' + AND RTRIM(p.instance_name) = '_Total' + AND p.cntr_value > 0 + AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; + END + + + IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) + BEGIN + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'http://BrentOzar.com/askbrent/plan-cache-erased-recently/', + 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' + FROM sys.dm_exec_query_stats WITH (NOLOCK) + ORDER BY creation_time + END; + + IF EXISTS (SELECT * FROM sys.configurations WHERE name = 'priority boost' AND (value = 1 OR value_in_use = 1)) + BEGIN + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'http://BrentOzar.com/go/priorityboost/', + 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.') + END; + + IF EXISTS (select * from msdb.dbo.backupset WHERE database_name = 'ReportServerTempDB') + BEGIN + INSERT INTO #BlitzResults + (CheckID, + Priority, + DatabaseName, + FindingsGroup, + Finding, + URL, + Details) + VALUES(127, 200, 'ReportServerTempDB', 'Backup', 'Backing Up Unneeded Database', 'http://BrentOzar.com/go/reportservertempdb/', + 'This database is being backed up, but you probably do not need to. See the URL for more details on how to reconstruct it.') + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 128 ) + BEGIN + + IF (@ProductVersionMajor = 12 AND @ProductVersionMinor < 2000) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor <= 2100) OR + (@ProductVersionMajor = 10.5 AND @ProductVersionMinor <= 2500) OR + (@ProductVersionMajor = 10 AND @ProductVersionMinor <= 4000) OR + (@ProductVersionMajor = 9 AND @ProductVersionMinor <= 5000) + BEGIN + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'http://BrentOzar.com/go/unsupported', + 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.'); + END; + + END; + + /* Reliability - Dangerous Build of SQL Server (Corruption) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 129 ) + BEGIN + IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2342) + BEGIN + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(129, 20, 'Reliability', 'Dangerous Build of SQL Server (Corruption)', 'http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds', + 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); + END; + + END; + + /* Reliability - Dangerous Build of SQL Server (Security) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 157 ) + BEGIN + IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR + (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR + (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4000 AND @ProductVersionMinor <= 4017) OR + (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4251 AND @ProductVersionMinor <= 4319) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3129) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3300 AND @ProductVersionMinor <= 3447) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2253) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2300 AND @ProductVersionMinor <= 2370) + BEGIN + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(157, 20, 'Reliability', 'Dangerous Build of SQL Server (Security)', 'https://technet.microsoft.com/en-us/library/security/MS14-044', + 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); + END; + + END; + + + /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 145 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_db_xtp_table_memory_stats' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 145 AS CheckID, + 10 AS Priority, + ''Performance'' AS FindingsGroup, + ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, + ''http://BrentOzar.com/go/hekaton'' AS URL, + CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details + FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' + WHERE c.name = ''max server memory (MB)'' + GROUP BY c.value_in_use + HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) + OR SUM(mem.pages_kb / 1024.0) > 250000'; + EXECUTE(@StringToExecute); + END + + + /* Performance - In-Memory OLTP (Hekaton) In Use */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 146 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_db_xtp_table_memory_stats' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 146 AS CheckID, + 200 AS Priority, + ''Performance'' AS FindingsGroup, + ''In-Memory OLTP (Hekaton) In Use'' AS Finding, + ''http://BrentOzar.com/go/hekaton'' AS URL, + CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details + FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' + WHERE c.name = ''max server memory (MB)'' + GROUP BY c.value_in_use + HAVING SUM(mem.pages_kb / 1024.0) > 10'; + EXECUTE(@StringToExecute); + END + + /* In-Memory OLTP (Hekaton) - Transaction Errors */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 147 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_xtp_transaction_stats' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 147 AS CheckID, + 100 AS Priority, + ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, + ''Transaction Errors'' AS Finding, + ''http://BrentOzar.com/go/hekaton'' AS URL, + ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details + FROM sys.dm_xtp_transaction_stats + WHERE validation_failures <> 0 + OR dependencies_failed <> 0 + OR write_conflicts <> 0 + OR unique_constraint_violations <> 0;' + EXECUTE(@StringToExecute); + END + + + + /* Reliability - Database Files on Network File Shares */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 148 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 148 AS CheckID , + d.[name] AS DatabaseName , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Files on Network File Shares' AS Finding , + 'http://BrentOzar.com/go/nas' AS URL , + ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details + FROM sys.databases d + INNER JOIN sys.master_files mf ON d.database_id = mf.database_id + WHERE mf.physical_name LIKE '\\%' + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + /* Reliability - Database Files Stored in Azure */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 149 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 149 AS CheckID , + d.[name] AS DatabaseName , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Files Stored in Azure' AS Finding , + 'http://BrentOzar.com/go/azurefiles' AS URL , + ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details + FROM sys.databases d + INNER JOIN sys.master_files mf ON d.database_id = mf.database_id + WHERE mf.physical_name LIKE 'http://%' + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + END + + + /* Reliability - Errors Logged Recently in the Default Trace */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 150 ) + AND @TracePath IS NOT NULL + BEGIN + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 150 AS CheckID , + t.DatabaseName, + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Errors Logged Recently in the Default Trace' AS Finding , + 'http://BrentOzar.com/go/defaulttrace' AS URL , + CAST(t.TextData AS NVARCHAR(4000)) AS Details + FROM sys.fn_trace_gettable(@TracePath, DEFAULT) t + WHERE t.EventClass = 22 + AND t.Severity >= 17 + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + END + + + /* Performance - Log File Growths Slow */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 151 ) + AND @TracePath IS NOT NULL + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 151 AS CheckID , + t.DatabaseName, + 50 AS Priority , + 'Performance' AS FindingsGroup , + 'Log File Growths Slow' AS Finding , + 'http://BrentOzar.com/go/filegrowth' AS URL , + CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting log file autogrowth to a smaller increment.' AS Details + FROM sys.fn_trace_gettable(@TracePath, DEFAULT) t + WHERE t.EventClass = 93 + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + AND t.Duration > 15000000 + GROUP BY t.DatabaseName + HAVING COUNT(*) > 1 + END + + + /* Performance - Many Plans for One Query */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 160 ) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 160 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Many Plans for One Query'' AS Finding, + ''http://BrentOzar.com/go/parameterization'' AS URL, + ''More than 50 plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = ''dbid'' + GROUP BY qs.query_hash, pa.value + HAVING COUNT(DISTINCT plan_handle) > 50'; + EXECUTE(@StringToExecute); + END + + + /* Performance - High Number of Cached Plans */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 161 ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 161 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''High Number of Cached Plans'' AS Finding, + ''http://BrentOzar.com/go/planlimits'' AS URL, + ''Your server configuration is limited to '' + CAST(ht.buckets_count AS VARCHAR(20)) + '' '' + ht.name + '' plans, and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details + FROM sys.dm_os_memory_cache_hash_tables ht + INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type + where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) + AND cc.entries_count >= (3 * ht.buckets_count)'; + EXECUTE(@StringToExecute); + END + + + /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 155 ) + AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 155 AS CheckID , + 0 AS Priority , + 'Outdated sp_Blitz' AS FindingsGroup , + 'sp_Blitz is Over 6 Months Old' AS Finding , + 'http://www.BrentOzar.com/blitz/' AS URL , + 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details + END + + + /* Populate a list of database defaults. I'm doing this kind of oddly - + it reads like a lot of work, but this way it compiles & runs on all + versions of SQL Server. + */ + INSERT INTO #DatabaseDefaults + SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_read_committed_snapshot_on', 0, 133, 210, 'Read Committed Snapshot Isolation Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'http://BrentOzar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); + + DECLARE DatabaseDefaultsLoop CURSOR FOR + SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details + FROM #DatabaseDefaults + + OPEN DatabaseDefaultsLoop + FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails + WHILE @@FETCH_STATUS = 0 + BEGIN + + 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.[' + @CurrentName + '] <> ' + @CurrentDefaultValue + ' OR d.[' + @CurrentName + '] IS NULL);'; + EXEC (@StringToExecute); + + FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails + END + + CLOSE DatabaseDefaultsLoop + DEALLOCATE DatabaseDefaultsLoop; + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 163 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 163, + ''?'', + 10, + ''Performance'', + ''Query Store Disabled'', + ''http://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''ReportServer'', ''ReportServerTempDB'')'; + END + + + + IF @CheckUserDatabaseObjects = 1 + BEGIN + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 32 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 32, + ''?'', + 110, + ''Performance'', + ''Triggers on Tables'', + ''http://BrentOzar.com/go/trig'', + (''The ['' + DB_NAME() + ''] database has triggers on the '' + s.name + ''.'' + o.name + '' table.'') + 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'''; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 38 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 38, + ''?'', + 110, + ''Performance'', + ''Active Tables Without Clustered Indexes'', + ''http://BrentOzar.com/go/heaps'', + (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that are being actively queried.'') + FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id + INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id + INNER JOIN sys.databases sd ON sd.name = ''?'' + LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id + WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NOT NULL + AND sd.name <> ''tempdb'' AND o.is_ms_shipped = 0 AND o.type <> ''S'''; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 164 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'fn_validate_plan_guide') + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 164, + ''?'', + 20, + ''Reliability'', + ''Plan Guides Failing'', + ''http://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)'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 39 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 39, + ''?'', + 110, + ''Performance'', + ''Inactive Tables Without Clustered Indexes'', + ''http://BrentOzar.com/go/heaps'', + (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that have not been queried since the last restart. These may be backup tables carelessly left behind.'') + FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id + INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id + INNER JOIN sys.databases sd ON sd.name = ''?'' + LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id + WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NULL + AND sd.name <> ''tempdb'' AND o.is_ms_shipped = 0 AND o.type <> ''S'''; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 46 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 46, + ''?'', + 100, + ''Performance'', + ''Leftover Fake Indexes From Wizards'', + ''http://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'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 47 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 47, + ''?'', + 100, + ''Performance'', + ''Indexes Disabled'', + ''http://BrentOzar.com/go/ixoff'', + (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1'; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 48 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 48, + ''?'', + 100, + ''Performance'', + ''Foreign Keys Not Trusted'', + ''http://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'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 56 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 56, + ''?'', + 100, + ''Performance'', + ''Check Constraint Not Trusted'', + ''http://BrentOzar.com/go/trust'', + (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') + from [?].sys.check_constraints 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'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 95 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 95 AS CheckID, + ''?'' as DatabaseName, + 110 AS Priority, + ''Performance'' AS FindingsGroup, + ''Plan Guides Enabled'' AS Finding, + ''http://BrentOzar.com/go/guides'' AS URL, + (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details + FROM [?].sys.plan_guides WHERE is_disabled = 0' + END; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 60 ) + BEGIN + EXEC sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 60 AS CheckID, + ''?'' as DatabaseName, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Fill Factor Changed'', + ''http://brentozar.com/go/fillfactor'' AS URL, + ''The ['' + DB_NAME() + ''] database has objects with fill factor < 80%. This can cause memory and storage performance problems, but may also prevent page splits.'' + FROM [?].sys.indexes + WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 78 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 78, + ''?'', + 100, + ''Performance'', + ''Stored Procedure WITH RECOMPILE'', + ''http://BrentOzar.com/go/recompile'', + (''['' + DB_NAME() + ''].['' + SPECIFIC_SCHEMA + ''].['' + SPECIFIC_NAME + ''] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.'') + from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_DEFINITION LIKE N''%WITH RECOMPILE%'' AND SPECIFIC_NAME NOT LIKE ''sp_Blitz%%'';'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 86 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 20, ''Security'', ''Elevated Permissions on a Database'', ''http://BrentOzar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM [?].dbo.sysmembers m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessAdmin'' , ''db_securityadmin'' , ''db_ddladmin'')'; + END + + + /*Check for non-aligned indexes in partioned databases*/ + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 72 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + insert into #partdb(dbname, objectname, type_desc) + SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc + FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id + JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id + LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() + WHERE o.type = ''u'' + -- Clustered and Non-Clustered indexes + AND i.type IN (1, 2) + AND o.object_id in + ( + SELECT a.object_id from + (SELECT ob.object_id, ds.type_desc from sys.objects ob JOIN sys.indexes ind on ind.object_id = ob.object_id join sys.data_spaces ds on ds.data_space_id = ind.data_space_id + GROUP BY ob.object_id, ds.type_desc ) a group by a.object_id having COUNT (*) > 1 + )' + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 72 AS CheckID , + dbname AS DatabaseName , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'The partitioned database ' + dbname + + ' may have non-aligned indexes' AS Finding , + 'http://BrentOzar.com/go/aligned' AS URL , + 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details + FROM #partdb + WHERE dbname IS NOT NULL + AND dbname NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + DROP TABLE #partdb + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 113 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 113, + ''?'', + 50, + ''Reliability'', + ''Full Text Indexes Not Updating'', + ''http://BrentOzar.com/go/fulltext'', + (''At least one full text index in this database has not been crawled in the last week.'') + from [?].sys.fulltext_indexes i WHERE i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE())'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 115 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 115, + ''?'', + 110, + ''Performance'', + ''Parallelism Rocket Surgery'', + ''http://BrentOzar.com/go/makeparallel'', + (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') + from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'''; + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 122 ) + BEGIN + /* SQL Server 2012 and newer uses temporary stats for AlwaysOn Availability Groups, and those show up as user-created */ + IF EXISTS (SELECT * + FROM sys.all_columns c + INNER JOIN sys.all_objects o ON c.object_id = o.object_id + WHERE c.name = 'is_temporary' AND o.name = 'stats') + + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 122, + ''?'', + 200, + ''Performance'', + ''User-Created Statistics In Place'', + ''http://BrentOzar.com/go/userstats'', + (''['' + DB_NAME() + ''] has user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') + from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0'; + + ELSE + EXEC dbo.sp_MSforeachdb 'USE [?]; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 122, + ''?'', + 200, + ''Performance'', + ''User-Created Statistics In Place'', + ''http://BrentOzar.com/go/userstats'', + (''['' + DB_NAME() + ''] has user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') + from [?].sys.stats WHERE user_created = 1'; + + + END /* IF NOT EXISTS ( SELECT 1 */ + + + END /* IF @CheckUserDatabaseObjects = 1 */ + + IF @CheckProcedureCache = 1 + BEGIN + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 35 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 35 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Single-Use Plans in Procedure Cache' AS Finding , + 'http://BrentOzar.com/go/single' AS URL , + ( CAST(COUNT(*) AS VARCHAR(10)) + + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details + FROM sys.dm_exec_cached_plans AS cp + WHERE cp.usecounts = 1 + AND cp.objtype = 'Adhoc' + AND EXISTS ( SELECT + 1 + FROM sys.configurations + WHERE + name = 'optimize for ad hoc workloads' + AND value_in_use = 0 ) + HAVING COUNT(*) > 1; + END + + + /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ + IF @@VERSION LIKE '%Microsoft SQL Server 2005%' + BEGIN + IF @CheckProcedureCacheFilter = 'CPU' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_worker_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'Reads' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_logical_reads DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'ExecCount' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.execution_count DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'Duration' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_elapsed_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + END; + IF @ProductVersionMajor >= 10 + BEGIN + IF @CheckProcedureCacheFilter = 'CPU' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_worker_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'Reads' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_logical_reads DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'ExecCount' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.execution_count DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + IF @CheckProcedureCacheFilter = 'Duration' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_elapsed_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL;' + EXECUTE(@StringToExecute) + END + + /* Populate the query_plan_filtered field. Only works in 2005SP2+, but we're just doing it in 2008 to be safe. */ + UPDATE #dm_exec_query_stats + SET query_plan_filtered = qp.query_plan + FROM #dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, + qs.statement_start_offset, + qs.statement_end_offset) + AS qp + + END; + + /* Populate the additional query_plan, text, and text_filtered fields */ + UPDATE #dm_exec_query_stats + SET query_plan = qp.query_plan , + [text] = st.[text] , + text_filtered = SUBSTRING(st.text, + ( qs.statement_start_offset + / 2 ) + 1, + ( ( CASE qs.statement_end_offset + WHEN -1 + THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END + - qs.statement_start_offset ) + / 2 ) + 1) + FROM #dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) + AS qp + + /* Dump instances of our own script. We're not trying to tune ourselves. */ + DELETE #dm_exec_query_stats + WHERE text LIKE '%sp_Blitz%' + OR text LIKE '%#BlitzResults%' + + /* Look for implicit conversions */ + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 63 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details , + QueryPlan , + QueryPlanFiltered + ) + SELECT 63 AS CheckID , + 120 AS Priority , + 'Query Plans' AS FindingsGroup , + 'Implicit Conversion' AS Finding , + 'http://BrentOzar.com/go/implicit' AS URL , + ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , + qs.query_plan , + qs.query_plan_filtered + FROM #dm_exec_query_stats qs + WHERE COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%CONVERT_IMPLICIT%' + AND COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%PhysicalOp="Index Scan"%' + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 64 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details , + QueryPlan , + QueryPlanFiltered + ) + SELECT 64 AS CheckID , + 120 AS Priority , + 'Query Plans' AS FindingsGroup , + 'Implicit Conversion Affecting Cardinality' AS Finding , + 'http://BrentOzar.com/go/implicit' AS URL , + ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , + qs.query_plan , + qs.query_plan_filtered + FROM #dm_exec_query_stats qs + WHERE COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= 11 + + BEGIN + EXEC sp_MSforeachdb N'USE [?]; + INSERT INTO #LogInfo2012 + EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; + IF @@ROWCOUNT > 999 + BEGIN + INSERT INTO #BlitzResults + ( CheckID + ,DatabaseName + ,Priority + ,FindingsGroup + ,Finding + ,URL + ,Details) + SELECT 69 + ,DB_NAME() + ,100 + ,''Performance'' + ,''High VLF Count'' + ,''http://BrentOzar.com/go/vlf'' + ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' + FROM #LogInfo2012 + WHERE EXISTS (SELECT name FROM master.sys.databases + WHERE source_database_id is null) ; + END + TRUNCATE TABLE #LogInfo2012;' + DROP TABLE #LogInfo2012; + END + ELSE + BEGIN + EXEC sp_MSforeachdb N'USE [?]; + INSERT INTO #LogInfo + EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; + IF @@ROWCOUNT > 999 + BEGIN + INSERT INTO #BlitzResults + ( CheckID + ,DatabaseName + ,Priority + ,FindingsGroup + ,Finding + ,URL + ,Details) + SELECT 69 + ,DB_NAME() + ,100 + ,''Performance'' + ,''High VLF Count'' + ,''http://BrentOzar.com/go/vlf'' + ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' + FROM #LogInfo + WHERE EXISTS (SELECT name FROM master.sys.databases + WHERE source_database_id is null); + END + TRUNCATE TABLE #LogInfo;' + DROP TABLE #LogInfo; + END + END + + /*Verify that the servername is set */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 70 ) + BEGIN + IF @@SERVERNAME IS NULL + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 70 AS CheckID , + 200 AS Priority , + 'Configuration' AS FindingsGroup , + '@@Servername Not Set' AS Finding , + 'http://BrentOzar.com/go/servername' AS URL , + '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details + END; + + IF /* @@SERVERNAME IS set */ + (@@SERVERNAME IS NOT NULL + AND + /* not a named instance */ + CHARINDEX('\',CAST(SERVERPROPERTY('ServerName') AS NVARCHAR)) = 0 + AND + /* not clustered, when computername may be different than the servername */ + SERVERPROPERTY('IsClustered') = 0 + AND + /* @@SERVERNAME is different than the computer name */ + @@SERVERNAME <> CAST(ISNULL(SERVERPROPERTY('ComputerNamePhysicalNetBIOS'),@@SERVERNAME) AS NVARCHAR) ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 70 AS CheckID , + 200 AS Priority , + 'Configuration' AS FindingsGroup , + '@@Servername Not Correct' AS Finding , + 'http://BrentOzar.com/go/servername' AS URL , + 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details + END; + + END + /*Check to see if a failsafe operator has been configured*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 73 ) + BEGIN + + DECLARE @AlertInfo TABLE + ( + FailSafeOperator NVARCHAR(255) , + NotificationMethod INT , + ForwardingServer NVARCHAR(255) , + ForwardingSeverity INT , + PagerToTemplate NVARCHAR(255) , + PagerCCTemplate NVARCHAR(255) , + PagerSubjectTemplate NVARCHAR(255) , + PagerSendSubjectOnly NVARCHAR(255) , + ForwardAlways INT + ) + INSERT INTO @AlertInfo + EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0 + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 73 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'No failsafe operator configured' AS Finding , + 'http://BrentOzar.com/go/failsafe' AS URL , + ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details + FROM @AlertInfo + WHERE FailSafeOperator IS NULL; + END + + /*Identify globally enabled trace flags*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 74 ) + BEGIN + INSERT INTO #TraceStatus + EXEC ( ' DBCC TRACESTATUS(-1) WITH NO_INFOMSGS' + ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + 'Global Trace Flag' AS FindingsGroup , + 'TraceFlag On' AS Finding , + 'http://www.BrentOzar.com/go/traceflags/' AS URL , + 'Trace flag ' + T.TraceFlag + + ' is enabled globally.' AS Details + FROM #TraceStatus T + END + + /*Check for transaction log file larger than data file */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 75 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 75 AS CheckID , + DB_NAME(a.database_id) , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Transaction Log Larger than Data File' AS Finding , + 'http://BrentOzar.com/go/biglog' AS URL , + 'The database [' + DB_NAME(a.database_id) + + '] has a transaction log file larger than a data file. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details + FROM sys.master_files a + WHERE a.type = 1 + AND DB_NAME(a.database_id) NOT IN ( + SELECT DISTINCT + DatabaseName + FROM #SkipChecks ) + AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ + AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) + FROM sys.master_files b + WHERE a.database_id = b.database_id + AND b.type = 0 + ) + AND a.database_id IN ( + SELECT database_id + FROM sys.databases + WHERE source_database_id IS NULL ) + END + + /*Check for collation conflicts between user databases and tempdb */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 76 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 76 AS CheckID , + name AS DatabaseName , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Collation is ' + collation_name AS Finding , + 'http://BrentOzar.com/go/collate' AS URL , + 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details + FROM sys.databases + WHERE name NOT IN ( 'master', 'model', 'msdb') + AND name NOT LIKE 'ReportServer%' + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL) + AND collation_name <> ( SELECT + collation_name + FROM + sys.databases + WHERE + name = 'tempdb' + ) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 77 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 77 AS CheckID , + dSnap.[name] AS DatabaseName , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Snapshot Online' AS Finding , + 'http://BrentOzar.com/go/snapshot' AS URL , + 'Database [' + dSnap.[name] + + '] is a snapshot of [' + + dOriginal.[name] + + ']. Make sure you have enough drive space to maintain the snapshot as the original database grows.' AS Details + FROM sys.databases dSnap + INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id + AND dSnap.name NOT IN ( + SELECT DISTINCT + DatabaseName + FROM + #SkipChecks ) + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 79 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 79 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Shrink Database Job' AS Finding , + 'http://BrentOzar.com/go/autoshrink' AS URL , + 'In the [' + j.[name] + '] job, step [' + + step.[step_name] + + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' AS Details + FROM msdb.dbo.sysjobs j + INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id + WHERE step.command LIKE N'%SHRINKDATABASE%' + OR step.command LIKE N'%SHRINKFILE%' + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 80 ) + BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 80, DB_NAME(), 50, ''Reliability'', ''Max File Size Set'', ''http://BrentOzar.com/go/maxsize'', (''The ['' + DB_NAME() + ''] database file '' + name + '' has a max file size set to '' + CAST(CAST(max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') FROM sys.database_files WHERE max_size <> 268435456 AND max_size <> -1 AND type <> 2'; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 81 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 81 AS CheckID , + 200 AS Priority , + 'Non-Active Server Config' AS FindingsGroup , + cr.name AS Finding , + 'http://www.BrentOzar.com/blitz/sp_configure/' AS URL , + ( 'This sp_configure option isn''t running under its set value. Its set value is ' + + CAST(cr.[Value] AS VARCHAR(100)) + + ' and its running value is ' + + CAST(cr.value_in_use AS VARCHAR(100)) + + '. When someone does a RECONFIGURE or restarts the instance, this setting will start taking effect.' ) AS Details + FROM sys.configurations cr + WHERE cr.value <> cr.value_in_use; + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 123 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 123 AS CheckID , + 200 AS Priority , + 'Performance' AS FindingsGroup , + 'Agent Jobs Starting Simultaneously' AS Finding , + 'http://BrentOzar.com/go/busyagent/' AS URL , + ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details + FROM msdb.dbo.sysjobactivity + WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) + GROUP BY start_execution_date HAVING COUNT(*) > 1; + END + + + IF @CheckServerInfo = 1 + BEGIN + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 130 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 130 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Server Name' AS Finding , + 'http://BrentOzar.com/go/servername' AS URL , + @@SERVERNAME AS Details + WHERE @@SERVERNAME IS NOT NULL; + END; + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 83 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 83 AS CheckID , + 250 AS Priority , + ''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(CAST(last_startup_time AS DATETIME) AS VARCHAR(50)), ''not shown.'') + ''. Startup type: '' + startup_type_desc + N'', currently '' + status_desc + ''.'' + FROM sys.dm_server_services;' + EXECUTE(@StringToExecute); + END + END + + /* Check 84 - SQL Server 2012 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 84 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'physical_memory_kb' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 84 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware'' AS Finding , + '''' AS URL , + ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_kb / 1024.0 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' + FROM sys.dm_os_sys_info'; + EXECUTE(@StringToExecute); + END + + /* Check 84 - SQL Server 2008 */ + IF EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'physical_memory_in_bytes' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 84 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware'' AS Finding , + '''' AS URL , + ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_in_bytes / 1024.0 / 1024 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' + FROM sys.dm_os_sys_info'; + EXECUTE(@StringToExecute); + END + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 85 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 85 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'SQL Server Service' AS Finding , + '' AS URL , + N'Version: ' + + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) + + N'. Patch Level: ' + + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) + + N'. Edition: ' + + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + + N'. AlwaysOn Enabled: ' + + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), + 0) AS VARCHAR(100)) + + N'. AlwaysOn Mgr Status: ' + + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), + 0) AS VARCHAR(100)) + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 88 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 88 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'SQL Server Last Restart' AS Finding , + '' AS URL , + CAST(create_date AS VARCHAR(100)) + FROM sys.databases + WHERE database_id = 2 + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 92 ) + BEGIN + INSERT INTO #driveInfo + ( drive, SIZE ) + EXEC master..xp_fixeddrives + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 92 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Drive ' + i.drive + ' Space' AS Finding , + '' AS URL , + CAST(i.SIZE AS VARCHAR) + + 'MB free on ' + i.drive + + ' drive' AS Details + FROM #driveInfo AS i + DROP TABLE #driveInfo + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 103 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'virtual_machine_type_desc' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 103 AS CheckID, + 250 AS Priority, + ''Server Info'' AS FindingsGroup, + ''Virtual Server'' AS Finding, + ''http://BrentOzar.com/go/virtual'' AS URL, + ''Type: ('' + virtual_machine_type_desc + '')'' AS Details + FROM sys.dm_os_sys_info + WHERE virtual_machine_type <> 0'; + EXECUTE(@StringToExecute); + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 114 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_os_memory_nodes' ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_nodes' + AND c.name = 'processor_group' ) + BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 114 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware - NUMA Config'' AS Finding , + '''' AS URL , + ''Node: '' + CAST(n.node_id AS NVARCHAR(10)) + '' State: '' + node_state_desc + + '' Online schedulers: '' + CAST(n.online_scheduler_count AS NVARCHAR(10)) + '' Processor Group: '' + CAST(n.processor_group AS NVARCHAR(10)) + + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) + FROM sys.dm_os_nodes n + INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id + WHERE n.node_state_desc NOT LIKE ''%DAC%'' + ORDER BY n.node_id' + EXECUTE(@StringToExecute); + END + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 106 ) + AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 + AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') + BEGIN + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 106 AS CheckID + ,250 AS Priority + ,'Server Info' AS FindingsGroup + ,'Default Trace Contents' AS Finding + ,'http://BrentOzar.com/go/trace' AS URL + ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as varchar)+' hours of data' + +' between '+cast(Min(StartTime) as varchar)+' and '+cast(GETDATE()as varchar) + +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + ) as Details + FROM ::fn_trace_gettable( @base_tracefilename, default ) + WHERE EventClass BETWEEN 65500 and 65600 + END /* CheckID 106 */ + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 152 ) + BEGIN + IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > .1 * @CPUMSsinceStartup AND waiting_tasks_count > 0 + AND wait_type NOT IN ('REQUEST_FOR_DEADLOCK_SEARCH', + 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', + 'SQLTRACE_BUFFER_FLUSH', + 'LAZYWRITER_SLEEP', + 'XE_TIMER_EVENT', + 'XE_DISPATCHER_WAIT', + 'FT_IFTS_SCHEDULER_IDLE_WAIT', + 'LOGMGR_QUEUE', + 'CHECKPOINT_QUEUE', + 'BROKER_TO_FLUSH', + 'BROKER_TASK_STOP', + 'BROKER_EVENTHANDLER', + 'SLEEP_TASK', + 'WAITFOR', + 'DBMIRROR_DBM_MUTEX', + 'DBMIRROR_EVENTS_QUEUE', + 'DBMIRRORING_CMD', + 'DISPATCHER_QUEUE_SEMAPHORE', + 'BROKER_RECEIVE_WAITFOR', + 'CLR_AUTO_EVENT', + 'DIRTY_PAGE_POLL', + 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', + 'ONDEMAND_TASK_QUEUE', + 'FT_IFTSHC_MUTEX', + 'CLR_MANUAL_EVENT', + 'CLR_SEMAPHORE', + 'DBMIRROR_WORKER_QUEUE', + 'DBMIRROR_DBM_EVENT', + 'SP_SERVER_DIAGNOSTICS_SLEEP', + 'HADR_CLUSAPI_CALL', + 'HADR_LOGCAPTURE_WAIT', + 'HADR_NOTIFICATION_DEQUEUE', + 'HADR_TIMER_TASK', + 'HADR_WORK_QUEUE', + 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', + 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', + 'REDO_THREAD_PENDING_WORK', + 'UCS_SESSION_REGISTRATION', + 'BROKER_TRANSMITTER')) + BEGIN + /* Check for waits that have had more than 10% of the server's wait time */ + WITH os(wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) + AS + (SELECT wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms + FROM sys.dm_os_wait_stats + WHERE wait_type NOT IN ('REQUEST_FOR_DEADLOCK_SEARCH', + 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', + 'SQLTRACE_BUFFER_FLUSH', + 'LAZYWRITER_SLEEP', + 'XE_TIMER_EVENT', + 'XE_DISPATCHER_WAIT', + 'FT_IFTS_SCHEDULER_IDLE_WAIT', + 'LOGMGR_QUEUE', + 'CHECKPOINT_QUEUE', + 'BROKER_TO_FLUSH', + 'BROKER_TASK_STOP', + 'BROKER_EVENTHANDLER', + 'SLEEP_TASK', + 'WAITFOR', + 'DBMIRROR_DBM_MUTEX', + 'DBMIRROR_EVENTS_QUEUE', + 'DBMIRRORING_CMD', + 'DISPATCHER_QUEUE_SEMAPHORE', + 'BROKER_RECEIVE_WAITFOR', + 'CLR_AUTO_EVENT', + 'DIRTY_PAGE_POLL', + 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', + 'ONDEMAND_TASK_QUEUE', + 'FT_IFTSHC_MUTEX', + 'CLR_MANUAL_EVENT', + 'CLR_SEMAPHORE', + 'DBMIRROR_WORKER_QUEUE', + 'DBMIRROR_DBM_EVENT', + 'SP_SERVER_DIAGNOSTICS_SLEEP', + 'HADR_CLUSAPI_CALL', + 'HADR_LOGCAPTURE_WAIT', + 'HADR_NOTIFICATION_DEQUEUE', + 'HADR_TIMER_TASK', + 'HADR_WORK_QUEUE', + 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', + 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP', + 'REDO_THREAD_PENDING_WORK', + 'UCS_SESSION_REGISTRATION', + 'BROKER_TRANSMITTER') + AND wait_time_ms > .1 * @CPUMSsinceStartup + AND waiting_tasks_count > 0) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 9 + 152 AS CheckID + ,240 AS Priority + ,'Wait Stats' AS FindingsGroup + , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding + ,'http://BrentOzar.com/go/waits' AS URL + , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(10,1)) AS NVARCHAR(20)) + N' hours of waits, ' + + CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MSSinceStartup AS NUMERIC(10,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + + CAST(CAST( + 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.wait_time_ms) OVER () ) + AS NUMERIC(10,1)) AS NVARCHAR(40)) + N'% of waits, ' + + CAST(CAST( + 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.wait_time_ms) OVER ()) + AS NUMERIC(10,1)) AS NVARCHAR(40)) + N'% signal wait, ' + + CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + + CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 + THEN + CAST( + SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) + AS NUMERIC(10,1)) + ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' + FROM os + ORDER BY SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) DESC; + END /* IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND waiting_tasks_count > 0) */ + + /* If no waits were found, add a note about that */ + IF NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (107, 108, 109, 121, 152, 162)) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'http://BrentOzar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); + END + END /* CheckID 152 */ + + END /* IF @CheckServerInfo = 1 */ + END /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ + + + /* Delete priorites they wanted to skip. */ + IF @IgnorePrioritiesAbove IS NOT NULL + DELETE #BlitzResults + WHERE [Priority] > @IgnorePrioritiesAbove AND CheckID <> -1; + + IF @IgnorePrioritiesBelow IS NOT NULL + DELETE #BlitzResults + WHERE [Priority] < @IgnorePrioritiesBelow AND CheckID <> -1; + + /* Delete checks they wanted to skip. */ + IF @SkipChecksTable IS NOT NULL + BEGIN + DELETE FROM #BlitzResults + WHERE DatabaseName IN ( SELECT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); + DELETE FROM #BlitzResults + WHERE CheckID IN ( SELECT CheckID + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); + DELETE r FROM #BlitzResults r + INNER JOIN #SkipChecks c ON r.DatabaseName = c.DatabaseName and r.CheckID = c.CheckID + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName')); + END + + /* Add summary mode */ + IF @SummaryMode > 0 + BEGIN + UPDATE #BlitzResults + SET Finding = br.Finding + ' (' + CAST(brTotals.recs AS NVARCHAR(20)) + ')' + FROM #BlitzResults br + INNER JOIN (SELECT FindingsGroup, Finding, Priority, COUNT(*) AS recs FROM #BlitzResults GROUP BY FindingsGroup, Finding, Priority) brTotals ON br.FindingsGroup = brTotals.FindingsGroup AND br.Finding = brTotals.Finding AND br.Priority = brTotals.Priority + WHERE brTotals.recs > 1; + + DELETE br + FROM #BlitzResults br + WHERE EXISTS (SELECT * FROM #BlitzResults brLower WHERE br.FindingsGroup = brLower.FindingsGroup AND br.Finding = brLower.Finding AND br.Priority = brLower.Priority AND br.ID > brLower.ID); + + END + + /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 255 , + 'Thanks!' , + 'From Brent Ozar Unlimited' , + 'http://www.BrentOzar.com/blitz/' , + 'Thanks from the Brent Ozar Unlimited team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.' + ); + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + + ) + VALUES ( -1 , + 0 , + 'sp_Blitz (TM) v' + CAST(@Version AS VARCHAR(20)) + ' as of ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), + 'From Brent Ozar Unlimited' , + 'http://www.BrentOzar.com/blitz/' , + 'Thanks from the Brent Ozar Unlimited team. We hope you found this tool useful, and if you need help relieving your SQL Server pains, email us at Help@BrentOzar.com.' + + ); + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + + ) + SELECT 156 , + 254 , + 'Rundate' , + GETDATE() , + 'http://www.BrentOzar.com/blitz/' , + 'Captain''s log: stardate something and something...'; + + IF @EmailRecipients IS NOT NULL + BEGIN + /* Database mail won't work off a local temp table. I'm not happy about this hacky workaround either. */ + IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; + SELECT * INTO ##BlitzResults FROM #BlitzResults; + SET @query_result_separator = char(9); + SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup, Finding, Details; SET NOCOUNT OFF;'; + SET @EmailSubject = 'sp_Blitz (TM) Results for ' + @@SERVERNAME; + SET @EmailBody = 'sp_Blitz (TM) v' + CAST(@Version AS VARCHAR(20)) + ' as of ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. From Brent Ozar Unlimited: http://www.BrentOzar.com/blitz/'; + IF @EmailProfile IS NULL + EXEC msdb.dbo.sp_send_dbmail + @recipients = @EmailRecipients, + @subject = @EmailSubject, + @body = @EmailBody, + @query_attachment_filename = 'sp_Blitz-Results.csv', + @attach_query_result_as_file = 1, + @query_result_header = 1, + @query_result_width = 32767, + @append_query_error = 1, + @query_result_no_padding = 1, + @query_result_separator = @query_result_separator, + @query = @StringToExecute; + ELSE + EXEC msdb.dbo.sp_send_dbmail + @profile_name = @EmailProfile, + @recipients = @EmailRecipients, + @subject = @EmailSubject, + @body = @EmailBody, + @query_attachment_filename = 'sp_Blitz-Results.csv', + @attach_query_result_as_file = 1, + @query_result_header = 1, + @query_result_width = 32767, + @append_query_error = 1, + @query_result_no_padding = 1, + @query_result_separator = @query_result_separator, + @query = @StringToExecute; + IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; + END + + + /* @OutputTableName lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIME, + BlitzVersion INT, + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + DatabaseName NVARCHAR(128), + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL, + CheckID INT , + CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + EXEC(@StringToExecute); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, BlitzVersion, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', GETDATE(), ' + CAST(@Version AS NVARCHAR(128)) + + ', CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; + EXEC(@StringToExecute); + END + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' + + 'CREATE TABLE ' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIME, + BlitzVersion INT, + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + DatabaseName NVARCHAR(128), + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL, + CheckID INT , + CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, BlitzVersion, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', GETDATE(), ' + CAST(@Version AS NVARCHAR(128)) + + ', CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; + EXEC(@StringToExecute); + END + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) + END + + + DECLARE @separator AS VARCHAR(1); + IF @OutputType = 'RSV' + SET @separator = CHAR(31); + ELSE + SET @separator = ','; + + IF @OutputType = 'COUNT' + BEGIN + SELECT COUNT(*) AS Warnings + FROM #BlitzResults + END + ELSE + IF @OutputType IN ( 'CSV', 'RSV' ) + BEGIN + + SELECT Result = CAST([Priority] AS NVARCHAR(100)) + + @separator + CAST(CheckID AS NVARCHAR(100)) + + @separator + COALESCE([FindingsGroup], + '(N/A)') + @separator + + COALESCE([Finding], '(N/A)') + @separator + + COALESCE(DatabaseName, '(N/A)') + @separator + + COALESCE([URL], '(N/A)') + @separator + + COALESCE([Details], '(N/A)') + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + Details; + END + ELSE IF @OutputXMLasNVARCHAR = 1 AND @OutputType <> 'NONE' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan, + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + Details; + END + ELSE IF @OutputType <> 'NONE' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + [QueryPlan] , + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + Details; + END + + DROP TABLE #BlitzResults; + + IF @OutputProcedureCache = 1 + AND @CheckProcedureCache = 1 + SELECT TOP 20 + total_worker_time / execution_count AS AvgCPU , + total_worker_time AS TotalCPU , + CAST(ROUND(100.00 * total_worker_time + / ( SELECT SUM(total_worker_time) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentCPU , + total_elapsed_time / execution_count AS AvgDuration , + total_elapsed_time AS TotalDuration , + CAST(ROUND(100.00 * total_elapsed_time + / ( SELECT SUM(total_elapsed_time) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + CAST(ROUND(100.00 * total_logical_reads + / ( SELECT SUM(total_logical_reads) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentReads , + execution_count , + CAST(ROUND(100.00 * execution_count + / ( SELECT SUM(execution_count) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentExecutions , + CASE WHEN DATEDIFF(mi, creation_time, + qs.last_execution_time) = 0 THEN 0 + ELSE CAST(( 1.00 * execution_count / DATEDIFF(mi, + creation_time, + qs.last_execution_time) ) AS MONEY) + END AS executions_per_minute , + qs.creation_time AS plan_creation_time , + qs.last_execution_time , + text , + text_filtered , + query_plan , + query_plan_filtered , + sql_handle , + query_hash , + plan_handle , + query_plan_hash + FROM #dm_exec_query_stats qs + ORDER BY CASE UPPER(@CheckProcedureCacheFilter) + WHEN 'CPU' THEN total_worker_time + WHEN 'READS' THEN total_logical_reads + WHEN 'EXECCOUNT' THEN execution_count + WHEN 'DURATION' THEN total_elapsed_time + ELSE total_worker_time + END DESC + + END /* ELSE -- IF @OutputType = 'SCHEMA' */ + + SET NOCOUNT OFF; +GO + +/* +--Sample execution call with the most common parameters: +EXEC [master].[dbo].[sp_Blitz] + @CheckUserDatabaseObjects = 1 , + @CheckProcedureCache = 0 , + @OutputType = 'TABLE' , + @OutputProcedureCache = 0 , + @CheckProcedureCacheFilter = NULL, + @CheckServerInfo = 1 +*/ diff --git a/Documentation/Development/Merge Blitz.ps1 b/Documentation/Development/Merge Blitz.ps1 index c288d288a..735a0903b 100644 --- a/Documentation/Development/Merge Blitz.ps1 +++ b/Documentation/Development/Merge Blitz.ps1 @@ -1,8 +1,24 @@ -#All Core Blitz Without sp_BlitzQueryStore -Get-ChildItem -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\*" -Include sp_Blitz*.sql -Exclude sp_BlitzQueryStore.sql | ForEach-Object { Get-Content $_.FullName } | Set-Content -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\Install-Core-Blitz-No-Query-Store.sql" -Force +#Set your file path +$FilePath = "/Users/brentozar/LocalOnly/Github/SQL-Server-First-Responder-Kit" +$SqlVersionsPath = "$FilePath/SqlServerVersions.sql" +$BlitzFirstPath = "$FilePath/sp_BlitzFirst.sql" -#All Core Blitz With sp_BlitzQueryStore -Get-ChildItem -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\*" -Include sp_Blitz*.sql | ForEach-Object { Get-Content $_.FullName } | Set-Content -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\Install-Core-Blitz-With-Query-Store.sql" -Force +#Azure - skip sp_Blitz, sp_BlitzBackups, sp_DatabaseRestore, sp_ineachdb +Get-ChildItem -Path "$FilePath" -Filter "sp_Blitz*.sql" | +Where-Object { $_.FullName -notlike "*sp_Blitz.sql*" -and $_.FullName -notlike "*sp_BlitzBackups*" -and $_.FullName -notlike "*sp_DatabaseRestore*"} | +ForEach-Object { Get-Content $_.FullName } | +Set-Content -Path "$FilePath/Install-Azure.sql" -Force +if ( test-path "$BlitzFirstPath") + { Add-Content -Path "$FilePath/Install-All-Scripts.sql" -Value (Get-Content -Path "$BlitzFirstPath")} + #All Scripts -Get-ChildItem -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\*" -Include sp_*.sql | ForEach-Object { Get-Content $_.FullName } | Set-Content -Path "C:\Users\$env:username\Documents\GitHub\PublicTools\Install-All-Scripts.sql" -Force \ No newline at end of file +Get-ChildItem -Path "$FilePath" -Filter "sp_*.sql" | +Where-Object { $_.FullName -notlike "*sp_BlitzInMemoryOLTP*" -and $_.FullName -notlike "*sp_BlitzFirst*"} | +ForEach-Object { Get-Content $_.FullName } | +Set-Content -Path "$FilePath/Install-All-Scripts.sql" -Force +#append script to (re-)create SqlServerVersions Table (https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2429) +if ( test-path "$SqlVersionsPath") + { Add-Content -Path "$FilePath/Install-All-Scripts.sql" -Value (Get-Content -Path "$SqlVersionsPath")} +if ( test-path "$BlitzFirstPath") + { Add-Content -Path "$FilePath/Install-All-Scripts.sql" -Value (Get-Content -Path "$BlitzFirstPath")} diff --git a/Documentation/Development/ReleaseProcess.md b/Documentation/Development/ReleaseProcess.md index 891384f63..becac9941 100644 --- a/Documentation/Development/ReleaseProcess.md +++ b/Documentation/Development/ReleaseProcess.md @@ -24,9 +24,10 @@ Well, useful is probably the wrong word. More like entertaining. Here we go. * Run _TestBed.sql: this has stored proc calls with common parameters. May have to add in new scenarios if new features are added. * If it passes, bump all the version numbers inside the scripts and re-run the PowerShell commands so combined scripts reflect correct version and date. * sp_foreachdb @Version and @VersionDate + * sp_ineachdb @Version and @VersionDate * sp_BlitzWho @Version and @VersionDate * sp_BlitzIndex @Version and @VersionDate - * sp_BlitzFirst @VersionDate (no version) + * sp_BlitzFirst @Version and @VersionDate * sp_BlitzCache @Version and @VersionDate * sp_Blitz @Version and @VersionDate * sp_DatabaseRestore @Version and @VersionDate @@ -34,6 +35,9 @@ Well, useful is probably the wrong word. More like entertaining. Here we go. * sp_BlitzQueryStore @Version and @VersionDate * sp_AllNightLog @Version and @VersionDate * sp_AllNightLog_Setup @Version and @VersionDate + * sp_BlitzInMemoryOLTP @Version and @VersionDate +* Update the SQL Server versions script from here: https://github.com/jadarnel27/SqlServerVersionScript + * Right now, you need to run it in Visual Studio, and paste the command window output into the script file ## Push to Master @@ -45,5 +49,24 @@ Well, useful is probably the wrong word. More like entertaining. Here we go. * Copy the FRK scripts into BrentOzar.com's First Responder Kit zip file (Employees/Products/First Responder Kit/FirstResponderKit.zip) * Copy the FirstResponderKit.zip into Employees/Public.BrentOzar -* Copy the Github release text into a WordPress blog post with the First Responder Kit category. The nice thing about doing the Github release first is that you should be able to copy/paste the Github release page and the HTML should paste smoothly into the WordPress draft window, complete with links to the Github issues. At the end of the post, put a download now link that points to: https://www.brentozar.com/first-aid/ +* For PowerBi, only keep the .pbit file +* Copy the Github release text into a WordPress blog post with the First Responder Kit Updates category. The nice thing about doing the Github release first is that you should be able to copy/paste the Github release page and the HTML should paste smoothly into the WordPress draft window, complete with links to the Github issues. At the end of the post, put a download now link that points to: https://www.brentozar.com/first-aid/ +* Blog post support wording: + +This goes at the top and bottom of each post: +You can download the updated FirstResponderKit.zip here. + +This goes at the end of each post: +

For Support

+ +When you have questions about how the tools work, talk with the community in the #FirstResponderKit Slack channel. If you need a free invite, hit SQLslack.com. Be patient - it's staffed with volunteers who have day jobs, heh. + +When you find a bug or want something changed, read the contributing.md file. + +When you have a question about what the scripts found, first make sure you read the "More Details" URL for any warning you find. We put a lot of work into documentation, and we wouldn't want someone to yell at you to go read the fine manual. After that, when you've still got questions about how something works in SQL Server, post a question at DBA.StackExchange.com and the community (that includes us!) will help. Include exact errors and any applicable screenshots, your SQL Server version number (including the build #), and the version of the tool you're working with. + +You can download the updated FirstResponderKit.zip here. + +## Update the Collector +Because you're a spaz and you always forget to do it. diff --git a/Documentation/Development/_TestBed.sql b/Documentation/Development/_TestBed.sql index 66bd1c3da..cea449c9f 100644 --- a/Documentation/Development/_TestBed.sql +++ b/Documentation/Development/_TestBed.sql @@ -1,51 +1,63 @@ /*Blitz*/ -EXEC sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1 - -EXEC sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1, @OutputDatabaseName = 'ChangeMe', @OutputSchemaName = 'dbo', @OutputTableName = 'Blitz' - -EXEC sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1, @Debug = 1 - -EXEC sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1, @Debug = 2 +EXEC dbo.sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1; +GO +EXEC dbo.sp_Blitz @CheckUserDatabaseObjects = 1, @CheckServerInfo = 1, @OutputDatabaseName = 'DBAtools', @OutputSchemaName = 'dbo', @OutputTableName = 'Blitz'; +GO + +/*BlitzWho*/ +EXEC dbo.sp_BlitzWho @ExpertMode = 1; +GO +EXEC dbo.sp_BlitzWho @ExpertMode = 0; +GO +EXEC dbo.sp_BlitzWho @OutputDatabaseName = 'DBAtools', @OutputSchemaName = 'dbo', @OutputTableName = 'BlitzWho_Results'; +GO /*BlitzFirst*/ -EXEC sp_BlitzFirst @Seconds = 5, @ExpertMode = 1 - -EXEC sp_BlitzFirst @SinceStartup = 1 +EXEC dbo.sp_BlitzFirst @Seconds = 5, @ExpertMode = 1; +GO +EXEC dbo.sp_BlitzFirst @SinceStartup = 1; +GO +EXEC dbo.sp_BlitzFirst @OutputDatabaseName = 'DBAtools', + @OutputSchemaName = 'dbo', + @OutputTableName = 'BlitzFirst', + @OutputTableNameFileStats = 'BlitzFirst_FileStats', + @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats', + @OutputTableNameWaitStats = 'BlitzFirst_WaitStats', + @OutputTableNameBlitzCache = 'BlitzCache', + @OutputTableNameBlitzWho = 'BlitzWho'; +GO -EXEC sp_BlitzFirst @Seconds = 5, @ExpertMode = 1, @ShowSleepingSPIDs = 1 /*BlitzIndex*/ -EXEC sp_BlitzIndex @GetAllDatabases = 1, @Mode = 4 - -EXEC sp_BlitzIndex @DatabaseName = 'StackOverflow', @Mode = 4 - -EXEC sp_BlitzIndex @DatabaseName = 'ChangeMe', @Mode = 4 - -EXEC sp_BlitzIndex @DatabaseName = 'StackOverflow', @Mode = 4, @SkipPartitions = 0, @SkipStatistics = 0 - -EXEC sp_BlitzIndex @DatabaseName = 'ChangeMe', @Mode = 4, @SkipPartitions = 0, @SkipStatistics = 0 - -EXEC sp_BlitzIndex @GetAllDatabases = 1, @Mode = 1 - -EXEC sp_BlitzIndex @GetAllDatabases = 1, @Mode = 2 - -EXEC sp_BlitzIndex @GetAllDatabases = 1, @Mode = 3 +EXEC dbo.sp_BlitzIndex @GetAllDatabases = 1, @Mode = 0; +GO +EXEC dbo.sp_BlitzIndex @GetAllDatabases = 1, @Mode = 1; +GO +EXEC dbo.sp_BlitzIndex @GetAllDatabases = 1, @Mode = 2; +GO +EXEC dbo.sp_BlitzIndex @GetAllDatabases = 1, @Mode = 3; +GO +EXEC dbo.sp_BlitzIndex @GetAllDatabases = 1, @Mode = 4; +GO +EXEC dbo.sp_BlitzIndex @DatabaseName = 'StackOverflow', @TableName = 'Users' +GO /*BlitzCache*/ -EXEC sp_BlitzCache @SortOrder = 'all', @Debug = 1 +EXEC dbo.sp_BlitzCache @SortOrder = 'all'; +GO +EXEC dbo.sp_BlitzCache @OutputDatabaseName = 'DBAtools', @OutputSchemaName = 'dbo', @OutputTableName = 'BlitzCache'; +GO -EXEC sp_BlitzCache @SortOrder = 'all avg' - -EXEC sp_BlitzCache @MinimumExecutionCount = 10 - -EXEC sp_BlitzCache @OutputDatabaseName = 'ChangeMe', @OutputSchemaName = 'dbo', @OutputTableName = 'BlitzCache' +/*BlitzQueryStore - uncomment this when testing on 2016+ instances: +EXEC dbo.sp_BlitzQueryStore @DatabaseName = 'StackOverflow'; +GO +*/ /*BlitzBackups*/ -EXEC sp_BlitzBackups @HoursBack = 1000000 - -/*sp_AllNightLog_Setup*/ -EXEC sp_AllNightLog_Setup @RPOSeconds = 30, @RTOSeconds = 30, @BackupPath = 'D:\Backup', @RestorePath = 'D:\Backup', @RunSetup = 1 +EXEC dbo.sp_BlitzBackups @HoursBack = 1000000; +GO /*sp_BlitzLock*/ -EXEC sp_BlitzLock @Debug = 1 \ No newline at end of file +EXEC dbo.sp_BlitzLock; +GO diff --git a/Documentation/sp_BlitzCache_Checks_by_Priority.md b/Documentation/sp_BlitzCache_Checks_by_Priority.md new file mode 100644 index 000000000..480bb5c72 --- /dev/null +++ b/Documentation/sp_BlitzCache_Checks_by_Priority.md @@ -0,0 +1,94 @@ +# sp_BlitzCache Checks by Priority + +This table lists all checks ordered by priority. + +Before adding a new check, make sure to add a Github issue for it first, and have a group discussion about its priority, description, and findings URL. + +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: 69 +If you want to add a new check, start at 70 + +| Priority | FindingsGroup | Finding | URL | CheckID | Expert Mode | +|----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------|-------------| +| 10 | Execution Plans | Forced Serialization | https://www.brentozar.com/blitzcache/forced-serialization/ | 25 | No | +| 10 | Large USERSTORE_TOKENPERM cache | Using Over 10% of the Buffer Pool | https://www.brentozar.com/go/userstore | 69 | No | +| 50 | Complexity | High Compile CPU | https://www.brentozar.com/blitzcache/high-compilers/ | 64 | No | +| 50 | Complexity | High Compile Memory | https://www.brentozar.com/blitzcache/high-compilers/ | 65 | No | +| 50 | Execution Plans | Compilation timeout | https://www.brentozar.com/blitzcache/compilation-timeout/ | 18 | No | +| 50 | Execution Plans | Compile Memory Limit Exceeded | https://www.brentozar.com/blitzcache/compile-memory-limit-exceeded/ | 19 | No | +| 50 | Execution Plans | No join predicate | https://www.brentozar.com/blitzcache/no-join-predicate/ | 20 | No | +| 50 | Execution Plans | Plan Warnings | https://www.brentozar.com/blitzcache/query-plan-warnings/ | 8 | No | +| 50 | Functions | Computed Column UDF | https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/ | 42 | Yes | +| 50 | Functions | Filter UDF | https://www.brentozar.com/blitzcache/compute-scalar-functions/ | 44 | Yes | +| 50 | Non-SARGable queries | Queries may have non-SARGable predicates |https://www.brentozar.com/go/sargable| 62 | No | +| 50 | Parameterization | Forced Parameterization | https://www.brentozar.com/blitzcache/forced-parameterization/ | 5 | No | +| 50 | Parameterization | Forced Plan | https://www.brentozar.com/blitzcache/forced-plans/ | 3 | No | +| 50 | Parameterization | Parameter Sniffing | https://www.brentozar.com/blitzcache/parameter-sniffing/ | 2 | No | +| 50 | Performance | Function Join | https://www.brentozar.com/blitzcache/tvf-join/ | 17 | Yes | +| 50 | Performance | Implicit Conversions | https://www.brentozar.com/go/implicit | 14 | No | +| 50 | Performance | Long Running Query | https://www.brentozar.com/blitzcache/long-running-queries/ | 9 | No | +| 50 | Performance | Missing Indexes | https://www.brentozar.com/blitzcache/missing-index-request/ | 10 | No | +| 50 | Selects w/ Writes | Read queries are causing writes | https://dba.stackexchange.com/questions/191825/ | 66 | No | +| 100 | Complexity | Long Compile Time | https://www.brentozar.com/blitzcache/high-compilers/ | No | +| 100 | Complexity | Many to Many Merge | Blog not published yet | 61 | Yes | +| 100 | Complexity | Row Estimate Mismatch | https://www.brentozar.com/blitzcache/bad-estimates/ | 56 | Yes | +| 100 | Compute Scalar That References A CLR Function | Calls CLR Functions | https://www.brentozar.com/blitzcache/compute-scalar-functions/| 31 | Yes | +| 100 | Compute Scalar That References A Function | Calls Functions | https://www.brentozar.com/blitzcache/compute-scalar-functions/| 31 | Yes | +| 100 | Execution Pattern | Frequently Execution | https://www.brentozar.com/blitzcache/frequently-executed-queries/ | 1 | No | +| 100 | Execution Plans | Expensive Key Lookup | https://www.brentozar.com/blitzcache/expensive-key-lookups/ | 26 | No | +| 100 | Execution Plans | Expensive Remote Query | https://www.brentozar.com/blitzcache/expensive-remote-query/ | 28 | | +| 100 | Execution Plans | Expensive Sort | https://www.brentozar.com/blitzcache/expensive-sorts/ | 43 | No | +| 100 | Execution Plans | Trivial Plans | https://www.brentozar.com/blitzcache/trivial-plans | 24 | No | +| 100 | Functions | MSTVFs | https://www.brentozar.com/blitzcache/tvf-join/ | 60 | No | +| 100 | Indexes | \>= 5 Indexes Modified | https://www.brentozar.com/blitzcache/many-indexes-modified/ | 45 | Yes | +| 100 | Indexes | ColumnStore Row Mode | https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/ | 41 | Yes | +| 100 | Indexes | Forced Indexes | https://www.brentozar.com/blitzcache/optimizer-forcing/ | 39 | Yes | +| 100 | Indexes | Forced Seeks/Scans | https://www.brentozar.com/blitzcache/optimizer-forcing/ | 40 | Yes | +| 100 | Indexes | Table Scans (Heaps) | https://www.brentozar.com/archive/2012/05/video-heaps/ | 37 | No | +| 100 | Memory Grant | Unused Memory Grant | https://www.brentozar.com/blitzcache/unused-memory-grants/ | 30 | No | +| 100 | Parameterization | Unparameterized Query | https://www.brentozar.com/blitzcache/unparameterized-queries | 23 | Yes | +| 100 | Performance | Frequently executed operators | https://www.brentozar.com/blitzcache/busy-loops/ | 16 | Yes | +| 100 | Performance | Unmatched Indexes | https://www.brentozar.com/blitzcache/unmatched-indexes | 22 | No | +| 100 | Statistics | Columns With No Statistics | https://www.brentozar.com/blitzcache/columns-no-statistics/ | 35 | No | +| 100 | Table Variables detected | Beware nasty side effects | https://www.brentozar.com/blitzcache/table-variables/ | 33 | No | +| 100 | TempDB | >500mb Spills | https://www.brentozar.com/blitzcache/tempdb-spills/ | 59 | No | +| 100 | Warnings | Operator Warnings | https://www.brentozar.com/blitzcache/query-plan-warnings/ | 36 | Yes | +| 150 | Blocking | Long Running Low CPU | https://www.brentozar.com/blitzcache/long-running-low-cpu/ | 50 | No | +| 150 | Complexity | Index DML | https://www.brentozar.com/blitzcache/index-dml/ | 48 | Yes | +| 150 | Complexity | Low Cost High CPU | https://www.brentozar.com/blitzcache/low-cost-high-cpu/ | 51 | No | +| 150 | Complexity | Table DML | https://www.brentozar.com/blitzcache/table-dml/ | 49 | Yes | +| 150 | Statistics | Statistics used have > 100k modifications in the last 7 days | https://www.brentozar.com/blitzcache/stale-statistics/ | 52 | No | +| 150 | Indexes | Expensive Index Spool | https://www.brentozar.com/blitzcache/eager-index-spools/ | 54 | No | +| 150 | Indexes | Expensive Index Spool | https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools | 67 | No | +| 150 | Indexes | Large Index Row Spool | https://www.brentozar.com/blitzcache/eager-index-spools/ | 55 | No | +| 150 | Indexes | Large Index Row Spool | https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools | 68 | No | +| 200 | Cardinality | Downlevel CE | https://www.brentozar.com/blitzcache/legacy-cardinality-estimator/ | 13 | No | +| 200 | Complexity | Adaptive Joins | https://www.brentozar.com/blitzcache/adaptive-joins/ | 53 | No | +| 200 | Complexity | Row Goals | https://www.brentozar.com/go/rowgoals/ | 58 | Yes | +| 200 | Complexity | Row Level Security | https://www.brentozar.com/blitzcache/row-level-security/ | 46 | Yes | +| 200 | Complexity | Spatial Index | https://www.brentozar.com/blitzcache/spatial-indexes/ | 47 | Yes | +| 200 | Cursors | Cursor | https://www.brentozar.com/blitzcache/cursors-found-slow-queries/ | 4 | No | +| 200 | Cursors | Dynamic Cursors | https://www.brentozar.com/blitzcache/cursors-found-slow-queries/ | 4 | No | +| 200 | Cursors | Fast Forward Cursors | https://www.brentozar.com/blitzcache/cursors-found-slow-queries/ | 4 | No | +| 200 | Cursors | Non-forward Only Cursors | https://www.brentozar.com/blitzcache/cursors-found-slow-queries/ | 4 | No | +| 200 | Cursors | Optimistic Cursors | https://www.brentozar.com/blitzcache/cursors-found-slow-queries/ | 4 | No | +| 200 | Database Level Statistics | Database has stats updated 7 days ago with more than 100k modifications | https://www.brentozar.com/blitzcache/stale-statistics/ | 997 | No | +| 200 | Execution Plans | Multiple Plans | https://www.brentozar.com/blitzcache/multiple-plans/ | 21 | No | +| 200 | Execution Plans | Nearly Parallel | https://www.brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/ | 7 | No | +| 200 | Execution Plans | Parallel | https://www.brentozar.com/blitzcache/parallel-plans-detected/ | 6 | No | +| 200 | Indexes | Backwards Scans | https://www.brentozar.com/blitzcache/backwards-scans/ | 38 | Yes | +| 200 | Is Paul White Electric? | This query has a Switch operator in it! | https://www.sql.kiwi/2013/06/hello-operator-my-switch-is-bored.html | 57 | Yes | +| 200 | Trace Flags | Session Level Trace Flags Enabled | https://www.brentozar.com/blitz/trace-flags-enabled-globally/ | 29 | No | +| 254 | Plan Cache Information | Breaks cache down by creation date (24/4/1 hrs) | None | 999 | No | +| 255 | Global Trace Flags Enabled | You have Global Trace Flags enabled on your server | https://www.brentozar.com/blitz/trace-flags-enabled-globally/ | 1000 | No | +| 255 | Need more help? | Paste your plan on the internet! | http://pastetheplan.com | 2147483646 | No | +| 255 | Thanks for using sp_BlitzCache! | From Your Community Volunteers | http://FirstResponderKit.org | 2147483647 | No | + + +## Blank row for the future +| | | | | | | + + + + diff --git a/Documentation/sp_BlitzFirst Checks by Priority.md b/Documentation/sp_BlitzFirst Checks by Priority.md deleted file mode 100644 index caafa43fa..000000000 --- a/Documentation/sp_BlitzFirst Checks by Priority.md +++ /dev/null @@ -1,47 +0,0 @@ -# sp_BlitzFirst Checks by Priority - -This table lists all checks ordered by priority. - -Before adding a new check, make sure to add a Github issue for it first, and have a group discussion about its priority, description, and findings URL. - -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. - -| Priority | FindingsGroup | Finding | URL | CheckID | -|----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| -| 0 | Outdated sp_BlitzFirst | sp_BlitzFirst is Over 6 Months Old | http://FirstResponderKit.org/ | 27 | -| 0 | Outdated or Missing sp_BlitzCache | Update Your sp_BlitzCache | http://FirstResponderKit.org/ | 36 | -| 1 | Logged Message | Logged from sp_BlitzFirst | http://FirstResponderKit.org | 38 | -| 1 | Maintenance Tasks Running | Backup Running | https://BrentOzar.com/askbrent/backups | 1 | -| 1 | Maintenance Tasks Running | DBCC CHECK* Running | https://BrentOzar.com/askbrent/dbcc | 2 | -| 1 | Maintenance Tasks Running | Restore Running | https://BrentOzar.com/askbrent/backups | 3 | -| 1 | Query Problems | Long-Running Query Blocking Others | https://BrentOzar.com/go/blocking | 5 | -| 1 | Query Problems | Query Rolling Back | https://BrentOzar.com/go/rollback | 9 | -| 1 | Query Problems | Sleeping Query with Open Transactions | https://BrentOzar.com/go/sleeping | 8 | -| 1 | SQL Server Internal Maintenance | Data File Growing | https://BrentOzar.com/go/instant | 4 | -| 1 | SQL Server Internal Maintenance | Log File Growing | https://BrentOzar.com/go/logsize | 13 | -| 1 | SQL Server Internal Maintenance | Log File Shrinking | https://BrentOzar.com/go/logsize | 14 | -| 10 | Server Performance | Poison Wait Detected | https://BrentOzar.com/go/poison | 30 | -| 10 | Server Performance | Target Memory Lower Than Max | https://BrentOzar.com/go/target | 35 | -| 40 | Table Problems | Forwarded Fetches/Sec High | https://BrentOzar.com/go/fetch | 29 | -| 50 | In-Memory OLTP | Garbage Collection in Progress | https://BrentOzar.com/go/garbage | 31 | -| 50 | Query Problems | Compilations/Sec High | https://BrentOzar.com/go/compile | 15 | -| 50 | Query Problems | Plan Cache Erased Recently | https://BrentOzar.com/go/freeproccache | 7 | -| 50 | Query Problems | Re-Compilations/Sec High | https://BrentOzar.com/go/recompile | 16 | -| 50 | Server Performance | High CPU Utilization | https://BrentOzar.com/go/cpu | 24 | -| 50 | Server Performance | High CPU Utilization - Non SQL Processes | https://BrentOzar.com/go/cpu | 28 | -| 50 | Server Performance | Page Life Expectancy Low | https://BrentOzar.com/go/ple | 10 | -| 50 | Server Performance | Slow Data File Reads | https://BrentOzar.com/go/slow | 11 | -| 50 | Server Performance | Slow Log File Writes | https://BrentOzar.com/go/slow | 12 | -| 50 | Server Performance | Too Much Free Memory | https://BrentOzar.com/go/freememory | 34 | -| 100 | In-Memory OLTP | Transactions aborted | https://BrentOzar.com/go/aborted | 32 | -| 100 | Query Problems | Suboptimal Plans/Sec High | https://BrentOzar.com/go/suboptimal | 33 | -| 200 | Wait Stats | (One per wait type) | https://BrentOzar.com/sql/wait-stats/#(waittype) | 6 | -| 210 | Query Stats | Plan Cache Analysis Skipped | https://BrentOzar.com/go/topqueries | 18 | -| 210 | Query Stats | Top Resource-Intensive Queries | https://BrentOzar.com/go/topqueries | 17 | -| 250 | Server Info | Batch Requests per Second | https://BrentOzar.com/go/measure | 19 | -| 250 | Server Info | Re-Compiles per Second | https://BrentOzar.com/go/measure | 26 | -| 250 | Server Info | SQL Compilations/sec | https://BrentOzar.com/go/measure | 25 | -| 250 | Server Info | Wait Time per Core per Second | https://BrentOzar.com/go/measure | 20 | -| 251 | Server Info | CPU Utilization | | 23 | -| 251 | Server Info | Database Count | | 22 | -| 251 | Server Info | Database Size, Total GB | | 21 | diff --git a/Documentation/sp_BlitzFirst_Checks_by_Priority.md b/Documentation/sp_BlitzFirst_Checks_by_Priority.md new file mode 100644 index 000000000..433d096fe --- /dev/null +++ b/Documentation/sp_BlitzFirst_Checks_by_Priority.md @@ -0,0 +1,65 @@ +# sp_BlitzFirst Checks by Priority + +This table lists all checks ordered by priority. + +Before adding a new check, make sure to add a Github issue for it first, and have a group discussion about its priority, description, and findings URL. + +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: 53 +If you want to add a new check, start at 54. + +| Priority | FindingsGroup | Finding | URL | CheckID | +|----------|---------------------------------|---------------------------------------|-------------------------------------------------|----------| +| 0 | Outdated sp_BlitzFirst | sp_BlitzFirst is Over 6 Months Old | http://FirstResponderKit.org/ | 27 | +| 0 | Outdated or Missing sp_BlitzCache | Update Your sp_BlitzCache | http://FirstResponderKit.org/ | 36 | +| 1 | Logged Message | Logged from sp_BlitzFirst | http://FirstResponderKit.org | 38 | +| 1 | Maintenance Tasks Running | Backup Running | https://www.brentozar.com/askbrent/backups | 1 | +| 1 | Maintenance Tasks Running | DBCC CHECK* Running | https://www.brentozar.com/askbrent/dbcc | 2 | +| 1 | Maintenance Tasks Running | Restore Running | https://www.brentozar.com/askbrent/backups | 3 | +| 1 | Query Problems | Long-Running Query Blocking Others | https://www.brentozar.com/go/blocking | 5 | +| 1 | Query Problems | Query Rolling Back | https://www.brentozar.com/go/rollback | 9 | +| 1 | Query Problems | Sleeping Query with Open Transactions | https://www.brentozar.com/go/sleeping | 8 | +| 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 | +| 40 | Table Problems | Forwarded Fetches/Sec High | https://www.brentozar.com/go/fetch | 29 | +| 50 | In-Memory OLTP | Garbage Collection in Progress | https://www.brentozar.com/go/garbage | 31 | +| 50 | Query Problems | Compilations/Sec High | https://www.brentozar.com/go/compile | 15 | +| 50 | Query Problems | Implicit Transactions | https://www.brentozar.com/go/ImplicitTransactions/ | 37 | +| 50 | Query Problems | Memory Leak in USERSTORE_TOKENPERM Cache | https://www.brentozar.com/go/userstore | 45 | +| 50 | Query Problems | Plan Cache Erased Recently | https://www.brentozar.com/go/freeproccache | 7 | +| 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 | +| 50 | Server Performance | Slow Log File Writes | https://www.brentozar.com/go/slow | 12 | +| 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 | Bad Estimates | https://www.brentozar.com/go/skewedup | 42 | +| 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 | +| 210 | Query Stats | Top Resource-Intensive Queries | https://www.brentozar.com/go/topqueries | 17 | +| 250 | Server Info | Batch Requests per Second | https://www.brentozar.com/go/measure | 19 | +| 250 | Server Info | Re-Compiles per Second | https://www.brentozar.com/go/measure | 26 | +| 250 | Server Info | SQL Compilations/sec | https://www.brentozar.com/go/measure | 25 | +| 250 | Server Info | Wait Time per Core per Second | https://www.brentozar.com/go/measure | 20 | +| 251 | Server Info | CPU Utilization | | 23 | +| 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/Documentation/sp_BlitzIndex_Checks_by_Priority.md b/Documentation/sp_BlitzIndex_Checks_by_Priority.md new file mode 100644 index 000000000..04ecd4666 --- /dev/null +++ b/Documentation/sp_BlitzIndex_Checks_by_Priority.md @@ -0,0 +1,77 @@ +# sp_BlitzIndex Checks by Priority + +This table lists all checks ordered by priority. + +Before adding a new check, make sure to add a Github issue for it first, and have a group discussion about its priority, description, and findings URL. + +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: 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 | 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/Documentation/sp_Blitz Checks by Priority.md b/Documentation/sp_Blitz_Checks_by_Priority.md similarity index 81% rename from Documentation/sp_Blitz Checks by Priority.md rename to Documentation/sp_Blitz_Checks_by_Priority.md index 37d0c2843..950d11b87 100644 --- a/Documentation/sp_Blitz Checks by Priority.md +++ b/Documentation/sp_Blitz_Checks_by_Priority.md @@ -6,21 +6,34 @@ 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: 272. +If you want to add a new one, start at 273. + | Priority | FindingsGroup | Finding | URL | CheckID | |----------|-----------------------------|---------------------------------------------------------|------------------------------------------------------------------------|----------| | 0 | Outdated sp_Blitz | sp_Blitz is Over 6 Months Old | https://www.BrentOzar.com/blitz/ | 155 | +| 0 | Informational | @CheckUserDatabaseObjects Disabled | https://www.BrentOzar.com/blitz/ | 201 | | 0 | Informational | @CheckUserDatabaseObjects Disabled | https://www.BrentOzar.com/blitz/ | 204 | +| 0 | Informational | Some Checks Skipped | https://www.BrentOzar.com/blitz/ | 223 | | 1 | Backup | Backing Up to Same Drive Where Databases Reside | https://www.BrentOzar.com/go/backup | 93 | | 1 | Backup | Backups Not Performed Recently | https://www.BrentOzar.com/go/nobak | 1 | | 1 | Backup | Encryption Certificate Not Backed Up Recently | https://www.BrentOzar.com/go/tde | 202 | | 1 | Backup | Full Recovery Mode w/o Log Backups | https://www.BrentOzar.com/go/biglogs | 2 | +| 1 | Backup | Log Backups to NUL | https://www.BrentOzar.com/go/nul | 256 | | 1 | Backup | TDE Certificate Not Backed Up Recently | https://www.BrentOzar.com/go/tde | 119 | | 1 | Corruption | Database Corruption Detected | https://www.BrentOzar.com/go/repair | 34 | | 1 | Corruption | Database Corruption Detected | https://www.BrentOzar.com/go/repair | 89 | | 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 | +| 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 | @@ -30,42 +43,43 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 10 | Performance | Auto-Shrink Ran Recently| https://www.BrentOzar.com/go/autoshrink | 206 | | 10 | Performance | CPU Schedulers Offline | https://www.BrentOzar.com/go/schedulers | 101 | | 10 | Performance | CPU w/Odd Number of Cores | https://www.BrentOzar.com/go/oddity | 198 | -| 10 | Performance | DBCC DROPCLEANBUFFERS Ran Recently | | 207 | -| 10 | Performance | DBCC FREEPROCCACHE Ran Recently | | 208 | -| 10 | Performance | DBCC SHRINK% Ran Recently | | 210 | +| 10 | Performance | DBCC DROPCLEANBUFFERS Ran Recently | https://www.BrentOzar.com/go/dbcc | 207 | +| 10 | Performance | DBCC FREEPROCCACHE Ran Recently | https://www.BrentOzar.com/go/dbcc | 208 | +| 10 | Performance | DBCC SHRINK% Ran Recently | https://www.BrentOzar.com/go/dbcc | 210 | | 10 | Performance | High Memory Use for In-Memory OLTP (Hekaton) | https://www.BrentOzar.com/go/hekaton | 145 | | 10 | Performance | Memory Nodes Offline | https://www.BrentOzar.com/go/schedulers | 110 | | 10 | Performance | Plan Cache Erased Recently | https://www.BrentOzar.com/askbrent/plan-cache-erased-recently/ | 125 | +| 10 | Reliability | DBCC WRITEPAGE Used Recently | https://www.BrentOzar.com/go/dbcc | 209 | +| 10 | Reliability | Server restarted in last 24 hours | | 221 | | 20 | Reliability | Dangerous Build of SQL Server (Corruption) | http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds | 129 | | 20 | Reliability | Dangerous Build of SQL Server (Security) | https://technet.microsoft.com/en-us/library/security/MS14-044 | 157 | | 20 | Reliability | Databases in Unusual States | https://www.BrentOzar.com/go/repair | 102 | | 20 | Reliability | Memory Dumps Have Occurred | https://www.BrentOzar.com/go/dump | 171 | | 20 | Reliability | No Failover Cluster Nodes Available | https://www.BrentOzar.com/go/node | 184 | -| 20 | Reliability | Plan Guides Failing | https://www.BrentOzar.com/go/guides | 164 | | 20 | Reliability | Query Store Cleanup Disabled | https://www.BrentOzar.com/go/cleanup | 182 | | 20 | Reliability | Unsupported Build of SQL Server | https://www.BrentOzar.com/go/unsupported | 128 | | 20 | Reliability | User Databases on C Drive | https://www.BrentOzar.com/go/cdrive | 26 | | 20 | Reliability | TempDB on C Drive | https://www.BrentOzar.com/go/cdrive | 25 | -| 50 | DBCC Events | Overall Events | | 203 | +| 50 | DBCC Events | Overall Events | https://www.BrentOzar.com/go/dbcc | 203 | | 50 | Performance | File Growths Slow | https://www.BrentOzar.com/go/filegrowth | 151 | | 50 | Performance | Instant File Initialization Not Enabled | https://www.BrentOzar.com/go/instant | 192 | +| 50 | Performance | Memory Leak in USERSTORE_TOKENPERM Cache | https://www.BrentOzar.com/go/userstore| 233 | | 50 | Performance | Poison Wait Detected | https://www.BrentOzar.com/go/poison | 107 | | 50 | Performance | Poison Wait Detected: CMEMTHREAD & NUMA | https://www.BrentOzar.com/go/poison | 162 | | 50 | Performance | Poison Wait Detected: Serializable Locking | https://www.BrentOzar.com/go/serializable | 121 | +| 50 | Performance | Recovery Interval Not Optimal| https://sqlperformance.com/2020/05/system-configuration/0-to-60-switching-to-indirect-checkpoints | 257 | +| 50 | Performance | Snapshotting Too Many Databases | https://www.BrentOzar.com/go/toomanysnaps | 236 | | 50 | Performance | Too Much Free Memory | https://www.BrentOzar.com/go/freememory | 165 | | 50 | Performance | Wait Stats Cleared Recently| | 205 | -| 50 | Reliability | Database Snapshot Online | https://www.BrentOzar.com/go/snapshot | 77 | -| 50 | Reliability | DBCC WRITEPAGE Used Recently | | 209 | -| 50 | Reliability | Errors Logged Recently in the Default Trace | https://www.BrentOzar.com/go/defaulttrace | 150 | | 50 | Reliability | Full Text Indexes Not Updating | https://www.BrentOzar.com/go/fulltext | 113 | | 50 | Reliability | Page Verification Not Optimal | https://www.BrentOzar.com/go/torn | 14 | | 50 | Reliability | Possibly Broken Log Shipping | https://www.BrentOzar.com/go/shipping | 111 | -| 50 | Reliability | Remote Admin Connections Disabled | https://www.BrentOzar.com/go/dac | 100 | | 50 | Reliability | TempDB File Error | https://www.BrentOzar.com/go/tempdboops | 191 | | 50 | Reliability | Transaction Log Larger than Data File | https://www.BrentOzar.com/go/biglog | 75 | -| 50 | Reliability | Default Trace File Error | https://BrentOzar.com/go/defaulttrace | 199 | +| 50 | Reliability | Default Trace File Error | https://www.brentozar.com/go/defaulttrace | 199 | | 100 | In-Memory OLTP (Hekaton) | Transaction Errors | https://www.BrentOzar.com/go/hekaton | 147 | -| 100 | Features | Missing Features | https://www.BrentOzar.com/ | 189 | +| 100 | Features | Missing Features (2016 SP1) | https://www.BrentOzar.com/ | 189 | +| 100 | Features | Missing Features (2017 CU3) | https://www.BrentOzar.com/ | 216 | | 100 | Performance | Change Tracking Enabled | https://www.BrentOzar.com/go/tracking | 112 | | 100 | Performance | Fill Factor Changed | https://www.BrentOzar.com/go/fillfactor | 60 | | 100 | Performance | High Number of Cached Plans | https://www.BrentOzar.com/go/planlimits | 161 | @@ -73,6 +87,7 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 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 | @@ -82,12 +97,17 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 100 | Performance | Single-Use Plans in Procedure Cache | https://www.BrentOzar.com/go/single | 35 | | 100 | Performance | Stored Procedure WITH RECOMPILE | https://www.BrentOzar.com/go/recompile | 78 | | 100 | Performance | Unusual SQL Server Edition | https://www.BrentOzar.com/go/workgroup | 97 | +| 100 | Performance | Implicit Transactions | https://www.brentozar.com/go/ImplicitTransactions/ | 215 | +| 100 | Reliability | Cumulative Update Available | https://SQLServerUpdates.com | 217 | +| 100 | Reliability | Plan Guides Failing | https://www.BrentOzar.com/go/guides | 164 | +| 100 | Reliability | SQL Server Update May Fail | https://desertdba.com/failovers-cant-serve-two-masters/ | 234 | | 110 | Performance | Active Tables Without Clustered Indexes | https://www.BrentOzar.com/go/heaps | 38 | | 110 | Performance | Auto-Create Stats Disabled | https://www.BrentOzar.com/go/acs | 15 | | 110 | Performance | Auto-Update Stats Disabled | https://www.BrentOzar.com/go/aus | 16 | | 110 | Performance | Infinite merge replication metadata retention period | https://www.BrentOzar.com/go/merge | 99 | | 110 | Performance | Parallelism Rocket Surgery | https://www.BrentOzar.com/go/makeparallel | 115 | | 110 | Performance | Plan Guides Enabled | https://www.BrentOzar.com/go/guides | 95 | +| 110 | Performance | Statistics Without Histograms | https://www.BrentOzar.com/go/brokenstats | 220 | | 120 | Query Plans | Cursor | https://www.BrentOzar.com/go/cursor | 66 | | 120 | Query Plans | Implicit Conversion | https://www.BrentOzar.com/go/implicit | 63 | | 120 | Query Plans | Implicit Conversion Affecting Cardinality | https://www.BrentOzar.com/go/implicit | 64 | @@ -98,27 +118,31 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 150 | Performance | Deadlocks Happening Daily | https://www.BrentOzar.com/go/deadlocks | 124 | | 150 | Performance | Forced Parameterization On | https://www.BrentOzar.com/go/forced | 18 | | 150 | Performance | Foreign Keys Not Trusted | https://www.BrentOzar.com/go/trust | 48 | -| 150 | Performance | Inactive Tables Without Clustered Indexes | https://www.BrentOzar.com/go/heaps | 39 | | 150 | Performance | Leftover Fake Indexes From Wizards | https://www.BrentOzar.com/go/hypo | 46 | +| 150 | Performance | Objects created with dangerous SET Options | https://www.BrentOzar.com/go/badset | 218 | | 150 | Performance | Queries Forcing Join Hints | https://www.BrentOzar.com/go/hints | 45 | | 150 | Performance | Queries Forcing Order Hints | https://www.BrentOzar.com/go/hints | 44 | | 150 | Performance | Slow Storage Reads on Drive | https://www.BrentOzar.com/go/slow | 36 | | 150 | Performance | Slow Storage Writes on Drive | https://www.BrentOzar.com/go/slow | 37 | | 150 | Performance | Stats Updated Asynchronously | https://www.BrentOzar.com/go/asyncstats | 17 | | 150 | Performance | Triggers on Tables | https://www.BrentOzar.com/go/trig | 32 | +| 150 | Performance | Inconsistent Query Store metadata | | 235 | | 170 | File Configuration | File growth set to 1MB | https://www.BrentOzar.com/go/percentgrowth | 158 | | 170 | File Configuration | File growth set to percent | https://www.BrentOzar.com/go/percentgrowth | 82 | | 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 | | 170 | File Configuration | Uneven File Growth Settings in One Filegroup | https://www.BrentOzar.com/go/grow | 42 | | 170 | Reliability | Database Files on Network File Shares | https://www.BrentOzar.com/go/nas | 148 | | 170 | Reliability | Database Files Stored in Azure | https://www.BrentOzar.com/go/azurefiles | 149 | +| 170 | Reliability | Database Snapshot Online | https://www.BrentOzar.com/go/snapshot | 77 | +| 170 | Reliability | Errors Logged Recently in the Default Trace | https://www.BrentOzar.com/go/defaulttrace | 150 | | 170 | Reliability | Max File Size Set | https://www.BrentOzar.com/go/maxsize | 80 | -| 200 | Backup | Backing Up Unneeded Database | https://www.BrentOzar.com/go/reportservertempdb | 127 | +| 170 | Reliability | Remote Admin Connections Disabled | https://www.BrentOzar.com/go/dac | 100 | | 200 | Backup | MSDB Backup History Not Purged | https://www.BrentOzar.com/go/history | 3 | | 200 | Backup | MSDB Backup History Purged Too Frequently | https://www.BrentOzar.com/go/history | 186 | | 200 | Informational | @@Servername not set | https://www.BrentOzar.com/go/servername | 70 | @@ -126,7 +150,6 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 200 | Informational | Backup Compression Default Off | https://www.BrentOzar.com/go/backup | 116 | | 200 | Informational | Cluster Node | https://www.BrentOzar.com/go/node | 53 | | 200 | Informational | Collation different than tempdb | https://www.BrentOzar.com/go/collate | 76 | -| 200 | Informational | Database Collation Mismatch | https://www.BrentOzar.com/go/collate | 58 | | 200 | Informational | Database Encrypted | https://www.BrentOzar.com/go/tde | 21 | | 200 | Informational | Date Correlation On | https://www.BrentOzar.com/go/corr | 20 | | 200 | Informational | Linked Server Configured | https://www.BrentOzar.com/go/link | 49 | @@ -134,12 +157,13 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 200 | Informational | Tables in the Master Database | https://www.BrentOzar.com/go/mastuser | 27 | | 200 | Informational | Tables in the Model Database | https://www.BrentOzar.com/go/model | 29 | | 200 | Informational | Tables in the MSDB Database | https://www.BrentOzar.com/go/msdbuser | 28 | -| 200 | Informational | TraceFlag On | https://www.BrentOzar.com/go/traceflags/ | 74 | +| 200 | Informational | TraceFlag On / Recommended Trace Flag Off | https://www.BrentOzar.com/go/traceflags/ | 74 | | 200 | Licensing | Enterprise Edition Features In Use | https://www.BrentOzar.com/go/ee | 33 | | 200 | Licensing | Non-Production License | https://www.BrentOzar.com/go/licensing | 173 | | 200 | Monitoring | Agent Jobs Without Failure Emails | https://www.BrentOzar.com/go/alerts | 94 | | 200 | Monitoring | Alerts Configured without Follow Up | https://www.BrentOzar.com/go/alert | 59 | | 200 | Monitoring | Alerts Disabled | https://www.BrentOzar.com/go/alerts/ | 98 | +| 200 | Monitoring | Alerts Without Event Descriptions | https://www.brentozar.com/go/alert | 219 | | 200 | Monitoring | Extended Events Hyperextension | https://www.BrentOzar.com/go/xe | 176 | | 200 | Monitoring | No Alerts for Corruption | https://www.BrentOzar.com/go/alert | 96 | | 200 | Monitoring | No Alerts for Sev 19-25 | https://www.BrentOzar.com/go/alert | 61 | @@ -221,15 +245,22 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 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 | | 200 | Performance | Non-Dynamic Memory | https://www.BrentOzar.com/go/memory | 190 | | 200 | Performance | Old Compatibility Level | https://www.BrentOzar.com/go/compatlevel | 62 | | 200 | Performance | Query Store Disabled | https://www.BrentOzar.com/go/querystore | 163 | +| 200 | Performance | Query Store Wait Stats Disabled | https://www.sqlskills.com/blogs/erin/query-store-settings/ | 262 | +| 200 | Performance | Query Store Effectively Disabled | https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify | 263 | +| 200 | Performance | Undesired Query Store State | https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify | 264 | +| 200 | Performance | Query Store Unusually Configured | https://www.sqlskills.com/blogs/erin/query-store-best-practices/ | 265 | | 200 | Performance | Snapshot Backups Occurring | https://www.BrentOzar.com/go/snaps | 178 | | 200 | Performance | User-Created Statistics In Place | https://www.BrentOzar.com/go/userstats | 122 | +| 200 | Performance | SSAS/SSIS/SSRS Installed | https://www.BrentOzar.com/go/services | 224 | | 200 | Reliability | Extended Stored Procedures in Master | https://www.BrentOzar.com/go/clr | 105 | +| 200 | Reliability | Resumable Index Operation Paused | https://www.BrentOzar.com/go/resumable | 225 | | 200 | Surface Area | Endpoints Configured | https://www.BrentOzar.com/go/endpoints/ | 9 | | 210 | Non-Default Database Config | ANSI NULL Default Enabled | https://www.BrentOzar.com/go/dbdefaults | 135 | | 210 | Non-Default Database Config | Auto Create Stats Incremental Enabled | https://www.BrentOzar.com/go/dbdefaults | 134 | @@ -238,19 +269,22 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 210 | Non-Default Database Config | Delayed Durability Enabled | https://www.BrentOzar.com/go/dbdefaults | 143 | | 210 | Non-Default Database Config | Forced Parameterization Enabled | https://www.BrentOzar.com/go/dbdefaults | 138 | | 210 | Non-Default Database Config | Memory Optimized Enabled | https://www.BrentOzar.com/go/dbdefaults | 144 | -| 210 | Non-Default Database Config | Query Store Enabled | https://www.BrentOzar.com/go/dbdefaults | 139 | | 210 | Non-Default Database Config | Read Committed Snapshot Isolation Enabled | https://www.BrentOzar.com/go/dbdefaults | 133 | | 210 | Non-Default Database Config | Recursive Triggers Enabled | https://www.BrentOzar.com/go/dbdefaults | 136 | | 210 | Non-Default Database Config | Snapshot Isolation Enabled | https://www.BrentOzar.com/go/dbdefaults | 132 | | 210 | Non-Default Database Config | Supplemental Logging Enabled | https://www.BrentOzar.com/go/dbdefaults | 131 | | 210 | Non-Default Database Config | Target Recovery Time Changed | https://www.BrentOzar.com/go/dbdefaults | 142 | | 210 | Non-Default Database Config | Trustworthy Enabled | https://www.BrentOzar.com/go/dbdefaults | 137 | +| 210 | Non-Default Database Config | Broker Enabled | https://www.BrentOzar.com/go/dbdefaults | 230 | +| 210 | Non-Default Database Config | Honor Broker Priority Enabled | https://www.BrentOzar.com/go/dbdefaults | 231 | | 210 | Non-Default Database Scoped Config | MAXDOP | https://www.BrentOzar.com/go/dbscope | 194 | | 210 | Non-Default Database Scoped Config | Legacy CE | https://www.BrentOzar.com/go/dbscope | 195 | | 210 | Non-Default Database Scoped Config | Parameter Sniffing | https://www.BrentOzar.com/go/dbscope | 196 | | 210 | Non-Default Database Scoped Config | Query Optimizer Hotfixes | https://www.BrentOzar.com/go/dbscope | 197 | +| 210 | Non-Default Database Scoped Config | All Others | https://www.BrentOzar.com/go/dbscope | 267 | | 230 | Security | Control Server Permissions | https://www.BrentOzar.com/go/sa | 104 | | 230 | Security | Database Owner <> SA | https://www.BrentOzar.com/go/owndb | 55 | +| 230 | Security | Database Owner is Unknown | | 213 | | 230 | Security | Elevated Permissions on a Database | https://www.BrentOzar.com/go/elevated | 86 | | 230 | Security | Endpoints Owned by Users | https://www.BrentOzar.com/go/owners | 187 | | 230 | Security | Jobs Owned By Users | https://www.BrentOzar.com/go/owners | 6 | @@ -259,16 +293,21 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 230 | Security | SQL Agent Job Runs at Startup | https://www.BrentOzar.com/go/startup | 57 | | 230 | Security | Stored Procedure Runs at Startup | https://www.BrentOzar.com/go/startup | 7 | | 230 | Security | Sysadmins | https://www.BrentOzar.com/go/sa | 4 | +| 230 | Security | Invalid Active Directory Accounts | | 2301| | 240 | Wait Stats | No Significant Waits Detected | https://www.BrentOzar.com/go/waits | 153 | | 240 | Wait Stats | Top Wait Stats | https://www.BrentOzar.com/go/waits | 152 | | 240 | Wait Stats | Wait Stats Have Been Cleared | https://www.BrentOzar.com/go/waits | 185 | | 250 | Informational | SQL Server Agent is running under an NT Service account | https://www.BrentOzar.com/go/setup | 170 | | 250 | Informational | SQL Server is running under an NT Service account | https://www.BrentOzar.com/go/setup | 169 | | 250 | Server Info | Agent is Currently Offline | | 167 | +| 250 | Server Info | Azure Managed Instance | https://www.BrenOzar.com/go/azurevm | 222 | +| 250 | Server Info | Container | https://www.BrentOzar.com/go/virtual | 214 | +| 250 | Server Info | Data Size | | 232 | | 250 | Server Info | Default Trace Contents | https://www.BrentOzar.com/go/trace | 106 | | 250 | Server Info | Drive Space | | 92 | | 250 | Server Info | Full-text Filter Daemon is Currently Offline | | 168 | | 250 | Server Info | Hardware | | 84 | +| 250 | Server Info | Hardware - Memory Counters | https://www.BrentOzar.com/go/target | 266 | | 250 | Server Info | Hardware - NUMA Config | | 114 | | 250 | Server Info | Instant File Initialization Enabled | https://www.BrentOzar.com/go/instant | 193 | | 250 | Server Info | Locked Pages in Memory Enabled | https://www.BrentOzar.com/go/lpim | 166 | @@ -279,4 +318,10 @@ If you want to change anything about a check - the priority, finding, URL, or ID | 250 | Server Info | SQL Server Service | | 85 | | 250 | Server Info | Virtual Server | https://www.BrentOzar.com/go/virtual | 103 | | 250 | Server Info | Windows Version | | 172 | +| 250 | Server Info | Power Plan | | 211 | +| 250 | Server Info | Stacked Instances | https://www.brentozar.com/go/babygotstacked/ | 212 | +| 253 | First Responder Kit | Version Check Failed | http://FirstResponderKit.org | 226 | +| 253 | First Responder Kit | Component Missing | http://FirstResponderKit.org | 227 | +| 253 | First Responder Kit | Component Outdated | http://FirstResponderKit.org | 228 | | 254 | Rundate | (Current Date) | | 156 | +| 255 | Thanks! | From Your Community Volunteers | | -1 | diff --git a/Install-All-Scripts.sql b/Install-All-Scripts.sql index 04942b0a0..f1ec7b551 100644 --- a/Install-All-Scripts.sql +++ b/Install-All-Scripts.sql @@ -1,3553 +1,5834 @@ -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -IF OBJECT_ID('dbo.sp_AllNightLog') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_AllNightLog AS RETURN 0;') +IF OBJECT_ID('dbo.sp_Blitz') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0;'); GO - -ALTER PROCEDURE dbo.sp_AllNightLog - @PollForNewDatabases BIT = 0, /* Formerly Pollster */ - @Backup BIT = 0, /* Formerly LogShaming */ - @PollDiskForNewDatabases BIT = 0, - @Restore BIT = 0, - @Debug BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT +ALTER PROCEDURE [dbo].[sp_Blitz] + @Help TINYINT = 0 , + @CheckUserDatabaseObjects TINYINT = 1 , + @CheckProcedureCache TINYINT = 0 , + @OutputType VARCHAR(20) = 'TABLE' , + @OutputProcedureCache TINYINT = 0 , + @CheckProcedureCacheFilter VARCHAR(10) = NULL , + @CheckServerInfo TINYINT = 0 , + @SkipChecksServer NVARCHAR(256) = NULL , + @SkipChecksDatabase NVARCHAR(256) = NULL , + @SkipChecksSchema NVARCHAR(256) = NULL , + @SkipChecksTable NVARCHAR(256) = NULL , + @IgnorePrioritiesBelow INT = NULL , + @IgnorePrioritiesAbove INT = NULL , + @OutputServerName NVARCHAR(256) = NULL , + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputXMLasNVARCHAR TINYINT = 0 , + @EmailRecipients VARCHAR(MAX) = NULL , + @EmailProfile sysname = NULL , + @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, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS -SET NOCOUNT ON; - -BEGIN; - -DECLARE @Version VARCHAR(30); -SET @Version = '2.0'; -SET @VersionDate = '20171201'; - -IF @Help = 1 - -BEGIN + SET NOCOUNT ON; + SET STATISTICS XML OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + - PRINT ' - /* + SELECT @Version = '8.26', @VersionDate = '20251002'; + SET @OutputType = UPPER(@OutputType); + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; - sp_AllNightLog from http://FirstResponderKit.org - - * @PollForNewDatabases = 1 polls sys.databases for new entries - * Unfortunately no other way currently to automate new database additions when restored from backups - * No triggers or extended events that easily do this - - * @Backup = 1 polls msdbCentral.dbo.backup_worker for databases not backed up in [RPO], takes LOG backups - * Will switch to a full backup if none exists - - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000! And really, maybe not even anything less than 2016. Heh. - - When restoring encrypted backups, the encryption certificate must already be installed. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @PollForNewDatabases BIT, defaults to 0. When this is set to 1, runs in a perma-loop to find new entries in sys.databases - @Backup BIT, defaults to 0. When this is set to 1, runs in a perma-loop checking the backup_worker table for databases that need to be backed up - @Debug BIT, defaults to 0. Whent this is set to 1, it prints out dynamic SQL commands - @RPOSeconds BIGINT, defaults to 30. Value in seconds you want to use to determine if a new log backup needs to be taken. - @BackupPath NVARCHAR(MAX), defaults to = ''D:\Backup''. You 99.99999% will need to change this path to something else. This tells Ola''s job where to put backups. - - For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. + IF @Help = 1 + BEGIN + PRINT ' + /* + sp_Blitz from http://FirstResponderKit.org - 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. + This script checks the health of your SQL Server and gives you a prioritized + to-do list of the most urgent things you should consider fixing. + 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. - */'; + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - If a database name has a question mark in it, some tests will fail. Gotta + love that unsupported sp_MSforeachdb. + - If you have offline databases, sp_Blitz fails the first time you run it, + but does work the second time. (Hoo, boy, this will be fun to debug.) + - @OutputServerName will output QueryPlans as NVARCHAR(MAX) since Microsoft + has refused to support XML columns in Linked Server queries. The bug is now + 16 years old! *~ \o/ ~* -RETURN -END + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) -DECLARE @database NVARCHAR(128) = NULL; --Holds the database that's currently being processed -DECLARE @error_number INT = NULL; --Used for TRY/CATCH -DECLARE @error_severity INT; --Used for TRY/CATCH -DECLARE @error_state INT; --Used for TRY/CATCH -DECLARE @msg NVARCHAR(4000) = N''; --Used for RAISERROR -DECLARE @rpo INT; --Used to hold the RPO value in our configuration table -DECLARE @rto INT; --Used to hold the RPO value in our configuration table -DECLARE @backup_path NVARCHAR(MAX); --Used to hold the backup path in our configuration table -DECLARE @changebackuptype NVARCHAR(MAX); --Config table: Y = escalate to full backup, MSDB = escalate if MSDB history doesn't show a recent full. -DECLARE @encrypt NVARCHAR(MAX); --Config table: Y = encrypt the backup. N (default) = do not encrypt. -DECLARE @encryptionalgorithm NVARCHAR(MAX); --Config table: native 2014 choices include TRIPLE_DES_3KEY, AES_128, AES_192, AES_256 -DECLARE @servercertificate NVARCHAR(MAX); --Config table: server certificate that is used to encrypt the backup -DECLARE @restore_path_base NVARCHAR(MAX); --Used to hold the base backup path in our configuration table -DECLARE @restore_path_full NVARCHAR(MAX); --Used to hold the full backup path in our configuration table -DECLARE @restore_path_log NVARCHAR(MAX); --Used to hold the log backup path in our configuration table -DECLARE @db_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL to create msdbCentral -DECLARE @tbl_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL that creates tables in msdbCentral -DECLARE @database_name NVARCHAR(256) = N'msdbCentral'; --Used to hold the name of the database we create to centralize data - --Right now it's hardcoded to msdbCentral, but I made it dynamic in case that changes down the line -DECLARE @cmd NVARCHAR(4000) = N'' --Holds dir cmd -DECLARE @FileList TABLE ( BackupFile NVARCHAR(255) ); --Where we dump @cmd -DECLARE @restore_full BIT = 0 --We use this one -DECLARE @only_logs_after NVARCHAR(30) = N'' + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + Parameter explanations: + @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. + @CheckServerInfo 1=show server info like CPUs, memory, virtualization + @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. + @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm + @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' + @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list (including server info, excluding security findings) | ''SCHEMA''=version and field list | ''XML'' =table output as XML | ''NONE'' = none + @IgnorePrioritiesBelow 50=ignore priorities below 50 + @IgnorePrioritiesAbove 50=ignore priorities above 50 + @Debug 0=silent (Default) | 1=messages per step | 2=outputs dynamic queries + For the rest of the parameters, see https://www.BrentOzar.com/blitz/documentation for details. -/* + MIT License + + Copyright for portions of sp_Blitz are held by Microsoft as part of project + tigertoolbox and are provided under the MIT license: + https://github.com/Microsoft/tigertoolbox + + All other copyrights for sp_Blitz are held by Brent Ozar Unlimited. -Make sure we're doing something + Copyright (c) Brent Ozar Unlimited -*/ + 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: -IF ( - @PollForNewDatabases = 0 - AND @PollDiskForNewDatabases = 0 - AND @Backup = 0 - AND @Restore = 0 - AND @Help = 0 -) - BEGIN - RAISERROR('You don''t seem to have picked an action for this stored procedure to take.', 0, 1) WITH NOWAIT - - RETURN; - END + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. -/* -Make sure xp_cmdshell is enabled -*/ -IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND value_in_use = 1) - BEGIN - RAISERROR('xp_cmdshell must be enabled so we can get directory contents to check for new databases to restore.', 0, 1) WITH NOWAIT - - RETURN; - END + 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. -/* -Make sure Ola Hallengren's scripts are installed in master -*/ -IF 2 <> (SELECT COUNT(*) FROM master.sys.procedures WHERE name IN('CommandExecute', 'DatabaseBackup')) - BEGIN - RAISERROR('Ola Hallengren''s CommandExecute and DatabaseBackup must be installed in the master database. More info: http://ola.hallengren.com', 0, 1) WITH NOWAIT - - RETURN; - END + */'; + RETURN; + END; /* @Help = 1 */ -/* -Make sure sp_DatabaseRestore is installed in master -*/ -IF NOT EXISTS (SELECT * FROM master.sys.procedures WHERE name = 'sp_DatabaseRestore') - BEGIN - RAISERROR('sp_DatabaseRestore must be installed in master. To get it: http://FirstResponderKit.org', 0, 1) WITH NOWAIT - - RETURN; - END + ELSE IF @OutputType = 'SCHEMA' + BEGIN + SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT'; + END;/* IF @OutputType = 'SCHEMA' */ + ELSE + BEGIN -IF (@PollDiskForNewDatabases = 1 OR @Restore = 1) AND OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL - BEGIN + DECLARE @StringToExecute NVARCHAR(4000) + ,@curr_tracefilename NVARCHAR(500) + ,@base_tracefilename NVARCHAR(500) + ,@indx int + ,@query_result_separator CHAR(1) + ,@EmailSubject NVARCHAR(255) + ,@EmailBody NVARCHAR(MAX) + ,@EmailAttachmentFilename NVARCHAR(255) + ,@ProductVersion NVARCHAR(128) + ,@ProductVersionMajor DECIMAL(10,2) + ,@ProductVersionMinor DECIMAL(10,2) + ,@CurrentName NVARCHAR(128) + ,@CurrentDefaultValue NVARCHAR(200) + ,@CurrentCheckID INT + ,@CurrentPriority INT + ,@CurrentFinding VARCHAR(200) + ,@CurrentURL VARCHAR(200) + ,@CurrentDetails NVARCHAR(4000) + ,@MsSinceWaitsCleared DECIMAL(38,0) + ,@CpuMsSinceWaitsCleared DECIMAL(38,0) + ,@ResultText NVARCHAR(MAX) + ,@crlf NVARCHAR(2) + ,@Processors int + ,@NUMANodes int + ,@MinServerMemory bigint + ,@MaxServerMemory bigint + ,@ColumnStoreIndexesInUse bit + ,@QueryStoreInUse bit + ,@TraceFileIssue bit + -- Flag for Windows OS to help with Linux support + ,@IsWindowsOperatingSystem BIT + ,@DaysUptime NUMERIC(23,2) + /* For First Responder Kit consistency check:*/ + ,@spBlitzFullName VARCHAR(1024) + ,@BlitzIsOutdatedComparedToOthers BIT + ,@tsql NVARCHAR(MAX) + ,@VersionCheckModeExistsTSQL NVARCHAR(MAX) + ,@BlitzProcDbName VARCHAR(256) + ,@ExecRet INT + ,@InnerExecRet INT + ,@TmpCnt INT + ,@PreviousComponentName VARCHAR(256) + ,@PreviousComponentFullPath VARCHAR(1024) + ,@CurrentStatementId INT + ,@CurrentComponentSchema VARCHAR(256) + ,@CurrentComponentName VARCHAR(256) + ,@CurrentComponentType VARCHAR(256) + ,@CurrentComponentVersionDate DATETIME2 + ,@CurrentComponentFullName VARCHAR(1024) + ,@CurrentComponentMandatory BIT + ,@MaximumVersionDate DATETIME + ,@StatementCheckName VARCHAR(256) + ,@StatementOutputsCounter BIT + ,@OutputCounterExpectedValue INT + ,@StatementOutputsExecRet BIT + ,@StatementOutputsDateTime BIT + ,@CurrentComponentMandatoryCheckOK BIT + ,@CurrentComponentVersionCheckModeOK BIT + ,@canExitLoop BIT + ,@frkIsConsistent BIT + ,@NeedToTurnNumericRoundabortBackOn BIT + ,@sa bit = 1 + ,@SUSER_NAME sysname = SUSER_SNAME() + ,@SkipDBCC bit = 0 + ,@SkipTrace bit = 0 + ,@SkipXPRegRead bit = 0 + ,@SkipXPFixedDrives bit = 0 + ,@SkipXPCMDShell bit = 0 + ,@SkipMaster bit = 0 + ,@SkipMSDB_objs bit = 0 + ,@SkipMSDB_jobs bit = 0 + ,@SkipModel bit = 0 + ,@SkipTempDB bit = 0 + ,@SkipValidateLogins bit = 0 + ,@SkipGetAlertInfo bit = 0 + + DECLARE + @db_perms table + ( + database_name sysname, + permission_name sysname + ); + + INSERT + @db_perms + ( + database_name, + permission_name + ) + SELECT + database_name = + DB_NAME(d.database_id), + fmp.permission_name + FROM sys.databases AS d + CROSS APPLY fn_my_permissions(d.name, 'DATABASE') AS fmp + WHERE fmp.permission_name = N'SELECT'; /*Databases where we don't have read permissions*/ + + /* End of declarations for First Responder Kit consistency check:*/ + ; - IF @Debug = 1 RAISERROR('Checking restore path', 0, 1) WITH NOWAIT; + /* Create temp table for check 73 */ + IF OBJECT_ID('tempdb..#AlertInfo') IS NOT NULL + EXEC sp_executesql N'DROP TABLE #AlertInfo;'; - SELECT @restore_path_base = CONVERT(NVARCHAR(512), configuration_setting) - FROM msdb.dbo.restore_configuration c - WHERE configuration_name = N'log restore path'; + CREATE TABLE #AlertInfo + ( + FailSafeOperator NVARCHAR(255) , + NotificationMethod INT , + ForwardingServer NVARCHAR(255) , + ForwardingSeverity INT , + PagerToTemplate NVARCHAR(255) , + PagerCCTemplate NVARCHAR(255) , + PagerSubjectTemplate NVARCHAR(255) , + PagerSendSubjectOnly NVARCHAR(255) , + ForwardAlways INT + ); + /* Create temp table for check 2301 */ + IF OBJECT_ID('tempdb..#InvalidLogins') IS NOT NULL + EXEC sp_executesql N'DROP TABLE #InvalidLogins;'; + + CREATE TABLE #InvalidLogins + ( + LoginSID varbinary(85), + LoginName VARCHAR(256) + ); - IF @restore_path_base IS NULL + /*Starting permissions checks here, but only if we're not a sysadmin*/ + IF + ( + SELECT + sa = + ISNULL + ( + IS_SRVROLEMEMBER(N'sysadmin'), + 0 + ) + ) = 0 + BEGIN + IF @Debug IN (1, 2) RAISERROR('User not SA, checking permissions', 0, 1) WITH NOWAIT; + + SET @sa = 0; /*Setting this to 0 to skip DBCC COMMANDS*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.fn_my_permissions(NULL, NULL) AS fmp + WHERE fmp.permission_name = N'VIEW SERVER STATE' + ) BEGIN - RAISERROR('@restore_path cannot be NULL. Please check the msdb.dbo.restore_configuration table', 0, 1) WITH NOWAIT; + RAISERROR('The user %s does not have VIEW SERVER STATE permissions.', 0, 11, @SUSER_NAME) WITH NOWAIT; RETURN; - END; + END; /*If we don't have this, we can't do anything at all.*/ - IF CHARINDEX('**', @restore_path_base) <> 0 + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(NULL, NULL) AS fmp + WHERE fmp.permission_name = N'ALTER TRACE' + ) BEGIN + SET @SkipTrace = 1; + END; /*We need this permission to execute trace stuff, apparently*/ - /* If they passed in a dynamic **DATABASENAME**, stop at that folder looking for databases. More info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/993 */ - IF CHARINDEX('**DATABASENAME**', @restore_path_base) <> 0 - BEGIN - SET @restore_path_base = SUBSTRING(@restore_path_base, 1, CHARINDEX('**DATABASENAME**',@restore_path_base) - 2); - END; - - SET @restore_path_base = REPLACE(@restore_path_base, '**AVAILABILITYGROUP**', ''); - SET @restore_path_base = REPLACE(@restore_path_base, '**BACKUPTYPE**', 'FULL'); - SET @restore_path_base = REPLACE(@restore_path_base, '**SERVERNAME**', REPLACE(CAST(SERVERPROPERTY('servername') AS nvarchar(max)),'\','$')); - - IF CHARINDEX('\',CAST(SERVERPROPERTY('servername') AS nvarchar(max))) > 0 - BEGIN - SET @restore_path_base = REPLACE(@restore_path_base, '**SERVERNAMEWITHOUTINSTANCE**', SUBSTRING(CAST(SERVERPROPERTY('servername') AS nvarchar(max)), 1, (CHARINDEX('\',CAST(SERVERPROPERTY('servername') AS nvarchar(max))) - 1))); - SET @restore_path_base = REPLACE(@restore_path_base, '**INSTANCENAME**', SUBSTRING(CAST(SERVERPROPERTY('servername') AS nvarchar(max)), CHARINDEX('\',CAST(SERVERPROPERTY('servername') AS nvarchar(max))), (LEN(CAST(SERVERPROPERTY('servername') AS nvarchar(max))) - CHARINDEX('\',CAST(SERVERPROPERTY('servername') AS nvarchar(max)))) + 1)); - END - ELSE /* No instance installed */ - BEGIN - SET @restore_path_base = REPLACE(@restore_path_base, '**SERVERNAMEWITHOUTINSTANCE**', CAST(SERVERPROPERTY('servername') AS nvarchar(max))); - SET @restore_path_base = REPLACE(@restore_path_base, '**INSTANCENAME**', 'DEFAULT'); - END + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'xp_fixeddrives', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipXPFixedDrives = 1; + END; /*Need execute on xp_fixeddrives*/ - IF CHARINDEX('**CLUSTER**', @restore_path_base) <> 0 - BEGIN - DECLARE @ClusterName NVARCHAR(128); - IF EXISTS(SELECT * FROM sys.all_objects WHERE name = 'dm_hadr_cluster') - BEGIN - SELECT @ClusterName = cluster_name FROM sys.dm_hadr_cluster; - END - SET @restore_path_base = REPLACE(@restore_path_base, '**CLUSTER**', COALESCE(@ClusterName,'')); - END; + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'xp_cmdshell', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipXPCMDShell = 1; + END; /*Need execute on xp_cmdshell*/ - END /* IF CHARINDEX('**', @restore_path_base) <> 0 */ - - END /* IF @PollDiskForNewDatabases = 1 OR @Restore = 1 */ + IF ISNULL(@SkipValidateLogins, 0) != 1 /*If @SkipValidateLogins hasn't been set to 1 by the caller*/ + BEGIN + BEGIN TRY + /* Try to fill the table for check 2301 */ + INSERT INTO #InvalidLogins + ( + [LoginSID] + ,[LoginName] + ) + EXEC sp_validatelogins; + + SET @SkipValidateLogins = 0; /*We can execute sp_validatelogins*/ + END TRY + BEGIN CATCH + SET @SkipValidateLogins = 1; /*We have don't have execute rights or sp_validatelogins throws an error so skip it*/ + END CATCH; + END; /*Need execute on sp_validatelogins*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'[master].[dbo].[sp_MSgetalertinfo]', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipGetAlertInfo = 1; + END; /*Need execute on sp_MSgetalertinfo*/ + IF ISNULL(@SkipModel, 0) != 1 /*If @SkipModel hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'model' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM model.sys.objects + ) + BEGIN + SET @SkipModel = 0; /*We have read permissions in the model database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipModel = 1; /*We have read permissions in the model database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipModel = 1; /*We don't have read permissions in the model database*/ + END; + END; -/* + IF ISNULL(@SkipMSDB_objs, 0) != 1 /*If @SkipMSDB_objs hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'msdb' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM msdb.sys.objects + ) + BEGIN + SET @SkipMSDB_objs = 0; /*We have read permissions in the msdb database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipMSDB_objs = 1; /*We have read permissions in the msdb database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipMSDB_objs = 1; /*We don't have read permissions in the msdb database*/ + END; + END; -Certain variables necessarily skip to parts of this script that are irrelevant -in both directions to each other. They are used for other stuff. - -*/ - - -/* - -Pollster use happens strictly to check for new databases in sys.databases to place them in a worker queue - -*/ + IF ISNULL(@SkipMSDB_jobs, 0) != 1 /*If @SkipMSDB_jobs hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'msdb' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM msdb.dbo.sysjobs + ) + BEGIN + SET @SkipMSDB_jobs = 0; /*We have read permissions in the msdb database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipMSDB_jobs = 1; /*We have read permissions in the msdb database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipMSDB_jobs = 1; /*We don't have read permissions in the msdb database*/ + END; + END; + END; -IF @PollForNewDatabases = 1 - GOTO Pollster; + SET @crlf = NCHAR(13) + NCHAR(10); + SET @ResultText = 'sp_Blitz Results: ' + @crlf; -/* + /* Last startup */ + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC(23, 2)) + FROM sys.databases + WHERE database_id = 2; + + IF @DaysUptime = 0 + SET @DaysUptime = .01; -LogShamer happens when we need to find and assign work to a worker job for backups + /* + Set the session state of Numeric_RoundAbort to off if any databases have Numeric Round-Abort enabled. + Stops arithmetic overflow errors during data conversion. See Github issue #2302 for more info. + */ + IF ( (8192 & @@OPTIONS) = 8192 ) /* Numeric RoundAbort is currently on, so we may need to turn it off temporarily */ + BEGIN + IF EXISTS (SELECT 1 + FROM sys.databases + WHERE is_numeric_roundabort_on = 1) /* A database has it turned on */ + BEGIN + SET @NeedToTurnNumericRoundabortBackOn = 1; + SET NUMERIC_ROUNDABORT OFF; + END; + END; + -*/ -IF @Backup = 1 - GOTO LogShamer; -/* + /* + --TOURSTOP01-- + See https://www.BrentOzar.com/go/blitztour for a guided tour. -Pollster use happens strictly to check for new databases in sys.databases to place them in a worker queue + We start by creating #BlitzResults. It's a temp table that will store all of + the results from our checks. Throughout the rest of this stored procedure, + we're running a series of checks looking for dangerous things inside the SQL + Server. When we find a problem, we insert rows into #BlitzResults. At the + end, we return these results to the end user. -*/ + #BlitzResults has a CheckID field, but there's no Check table. As we do + checks, we insert data into this table, and we manually put in the CheckID. + For a list of checks, visit http://FirstResponderKit.org. + */ + IF OBJECT_ID('tempdb..#BlitzResults') IS NOT NULL + DROP TABLE #BlitzResults; + CREATE TABLE #BlitzResults + ( + ID INT IDENTITY(1, 1) , + CheckID INT , + DatabaseName NVARCHAR(128) , + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL + ); -IF @PollDiskForNewDatabases = 1 - GOTO DiskPollster; + IF OBJECT_ID('tempdb..#TemporaryDatabaseResults') IS NOT NULL + DROP TABLE #TemporaryDatabaseResults; + CREATE TABLE #TemporaryDatabaseResults + ( + DatabaseName NVARCHAR(128) , + Finding NVARCHAR(128) + ); + /* First Responder Kit consistency (temporary tables) */ + + IF(OBJECT_ID('tempdb..#FRKObjects') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #FRKObjects;'; + END; + + -- this one represents FRK objects + CREATE TABLE #FRKObjects ( + DatabaseName VARCHAR(256) NOT NULL, + ObjectSchemaName VARCHAR(256) NULL, + ObjectName VARCHAR(256) NOT NULL, + ObjectType VARCHAR(256) NOT NULL, + MandatoryComponent BIT NOT NULL + ); + + + IF(OBJECT_ID('tempdb..#StatementsToRun4FRKVersionCheck') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #StatementsToRun4FRKVersionCheck;'; + END; -/* -Restoregasm Addict happens when we need to find and assign work to a worker job for restores + -- This one will contain the statements to be executed + -- order: 1- Mandatory, 2- VersionCheckMode, 3- VersionCheck -*/ + CREATE TABLE #StatementsToRun4FRKVersionCheck ( + StatementId INT IDENTITY(1,1), + CheckName VARCHAR(256), + SubjectName VARCHAR(256), + SubjectFullPath VARCHAR(1024), + StatementText NVARCHAR(MAX), + StatementOutputsCounter BIT, + OutputCounterExpectedValue INT, + StatementOutputsExecRet BIT, + StatementOutputsDateTime BIT + ); -IF @Restore = 1 - GOTO Restoregasm_Addict; + /* End of First Responder Kit consistency (temporary tables) */ + + + /* + You can build your own table with a list of checks to skip. For example, you + might have some databases that you don't care about, or some checks you don't + want to run. Then, when you run sp_Blitz, you can specify these parameters: + @SkipChecksDatabase = 'DBAtools', + @SkipChecksSchema = 'dbo', + @SkipChecksTable = 'BlitzChecksToSkip' + Pass in the database, schema, and table that contains the list of checks you + want to skip. This part of the code checks those parameters, gets the list, + and then saves those in a temp table. As we run each check, we'll see if we + need to skip it. + */ + /* --TOURSTOP07-- */ + IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL + DROP TABLE #SkipChecks; + CREATE TABLE #SkipChecks + ( + DatabaseName NVARCHAR(128) , + CheckID INT , + ServerName NVARCHAR(128) + ); + CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); + INSERT INTO #SkipChecks + (DatabaseName) + 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')) + OPTION(RECOMPILE); + /*Skip checks for database where we don't have read permissions*/ + INSERT INTO + #SkipChecks + ( + DatabaseName + ) + SELECT + DB_NAME(d.database_id) + FROM sys.databases AS d + WHERE NOT EXISTS + ( + SELECT + 1/0 + FROM @db_perms AS dp + WHERE dp.database_name = DB_NAME(d.database_id) + ); -/* + /*Skip individial checks where we don't have permissions*/ + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 29, NULL)) AS v (DatabaseName, CheckID, ServerName) /*Looks for user tables in model*/ + WHERE @SkipModel = 1; -Begin Polling section + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 28, NULL)) AS v (DatabaseName, CheckID, ServerName) /*Tables in the MSDB Database*/ + WHERE @SkipMSDB_objs = 1; -*/ + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES + /*sysjobs checks*/ + (NULL, 6, NULL), /*Jobs Owned By Users*/ + (NULL, 57, NULL), /*SQL Agent Job Runs at Startup*/ + (NULL, 79, NULL), /*Shrink Database Job*/ + (NULL, 94, NULL), /*Agent Jobs Without Failure Emails*/ + (NULL, 123, NULL), /*Agent Jobs Starting Simultaneously*/ + (NULL, 180, NULL), /*Shrink Database Step In Maintenance Plan*/ + (NULL, 181, NULL), /*Repetitive Maintenance Tasks*/ + + /*sysalerts checks*/ + (NULL, 30, NULL), /*Not All Alerts Configured*/ + (NULL, 59, NULL), /*Alerts Configured without Follow Up*/ + (NULL, 61, NULL), /*No Alerts for Sev 19-25*/ + (NULL, 96, NULL), /*No Alerts for Corruption*/ + (NULL, 98, NULL), /*Alerts Disabled*/ + (NULL, 219, NULL), /*Alerts Without Event Descriptions*/ + + /*sysoperators*/ + (NULL, 31, NULL) /*No Operators Configured/Enabled*/ + ) AS v (DatabaseName, CheckID, ServerName) + WHERE @SkipMSDB_jobs = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 68, NULL)) AS v (DatabaseName, CheckID, ServerName) /*DBCC command*/ + WHERE @sa = 0; + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 69, NULL)) AS v (DatabaseName, CheckID, ServerName) /*DBCC command*/ + WHERE @sa = 0; + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 92, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_fixeddrives*/ + WHERE @SkipXPFixedDrives = 1; -/* + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 106, NULL)) AS v (DatabaseName, CheckID, ServerName) /*alter trace*/ + WHERE @SkipTrace = 1; -This section runs in a loop checking for new databases added to the server, or broken backups + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 211, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_regread*/ + WHERE @sa = 0; -*/ + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 212, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_regread*/ + WHERE @SkipXPCMDShell = 1; + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 2301, NULL)) AS v (DatabaseName, CheckID, ServerName) /*sp_validatelogins*/ + WHERE @SkipValidateLogins = 1; -Pollster: + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 73, NULL)) AS v (DatabaseName, CheckID, ServerName) /*sp_validatelogins*/ + WHERE @SkipGetAlertInfo = 1; - IF @Debug = 1 RAISERROR('Beginning Pollster', 0, 1) WITH NOWAIT; - - IF OBJECT_ID('msdbCentral.dbo.backup_worker') IS NOT NULL - + IF @sa = 0 BEGIN - - WHILE @PollForNewDatabases = 1 - + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + '' AS URL , + 'User ''' + @SUSER_NAME + ''' is not part of the sysadmin role, so we skipped some checks that are not possible due to lack of permissions.' AS Details; + END; + /*End of SkipsChecks added due to permissions*/ + + IF @SkipChecksTable IS NOT NULL + AND @SkipChecksSchema IS NOT NULL + AND @SkipChecksDatabase IS NOT NULL BEGIN - BEGIN TRY - - IF @Debug = 1 RAISERROR('Checking for new databases...', 0, 1) WITH NOWAIT; - - /* - - Look for new non-system databases -- there should probably be additional filters here for accessibility, etc. + IF @Debug IN (1, 2) RAISERROR('Inserting SkipChecks', 0, 1) WITH NOWAIT; + + SET @StringToExecute = N'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) + SELECT DISTINCT DatabaseName, CheckID, ServerName + FROM ' + IF LTRIM(RTRIM(@SkipChecksServer)) <> '' + BEGIN + SET @StringToExecute += QUOTENAME(@SkipChecksServer) + N'.'; + END + SET @StringToExecute += QUOTENAME(@SkipChecksDatabase) + N'.' + QUOTENAME(@SkipChecksSchema) + N'.' + QUOTENAME(@SkipChecksTable) + + N' WHERE ServerName IS NULL OR ServerName = CAST(SERVERPROPERTY(''ServerName'') AS NVARCHAR(128)) OPTION (RECOMPILE);'; + EXEC(@StringToExecute); + END; - */ - - INSERT msdbCentral.dbo.backup_worker (database_name) - SELECT d.name - FROM sys.databases d - WHERE NOT EXISTS ( - SELECT 1 - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.database_name = d.name - ) - AND d.database_id > 4; + -- Flag for Windows OS to help with Linux support + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' ) + BEGIN + SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; + END; + ELSE + BEGIN + SELECT @IsWindowsOperatingSystem = 1 ; + END; - IF @Debug = 1 RAISERROR('Checking for wayward databases', 0, 1) WITH NOWAIT; - /* - - This section aims to find databases that have - * Had a log backup ever (the default for finish time is 9999-12-31, so anything with a more recent finish time has had a log backup) - * Not had a log backup start in the last 5 minutes (this could be trouble! or a really big log backup) - * Also checks msdb.dbo.backupset to make sure the database has a full backup associated with it (otherwise it's the first full, and we don't need to start taking log backups yet) + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 106 ) + AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 + BEGIN - */ - - IF EXISTS ( - - SELECT 1 - FROM msdbCentral.dbo.backup_worker bw WITH (READPAST) - WHERE bw.last_log_backup_finish_time < '99991231' - AND bw.last_log_backup_start_time < DATEADD(MINUTE, -5, GETDATE()) - AND EXISTS ( - SELECT 1 - FROM msdb.dbo.backupset b - WHERE b.database_name = bw.database_name - AND b.type = 'D' - ) - ) - - BEGIN - - IF @Debug = 1 RAISERROR('Resetting databases with a log backup and no log backup in the last 5 minutes', 0, 1) WITH NOWAIT; + select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; + set @curr_tracefilename = reverse(@curr_tracefilename); - - UPDATE bw - SET bw.is_started = 0, - bw.is_completed = 1, - bw.last_log_backup_start_time = '19000101' - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.last_log_backup_finish_time < '99991231' - AND bw.last_log_backup_start_time < DATEADD(MINUTE, -5, GETDATE()) - AND EXISTS ( - SELECT 1 - FROM msdb.dbo.backupset b - WHERE b.database_name = bw.database_name - AND b.type = 'D' - ); + -- Set the trace file path separator based on underlying OS + IF (@IsWindowsOperatingSystem = 1) AND @curr_tracefilename IS NOT NULL + BEGIN + select @indx = patindex('%\%', @curr_tracefilename) ; + set @curr_tracefilename = reverse(@curr_tracefilename) ; + set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '\log.trc' ; + END; + ELSE + BEGIN + select @indx = patindex('%/%', @curr_tracefilename) ; + set @curr_tracefilename = reverse(@curr_tracefilename) ; + set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '/log.trc' ; + END; - - END; --End check for wayward databases + END; - /* - - Wait 1 minute between runs, we don't need to be checking this constantly - - */ + /* If the server has any databases on Antiques Roadshow, skip the checks that would break due to CTEs. */ + IF @CheckUserDatabaseObjects = 1 AND EXISTS(SELECT * FROM sys.databases WHERE compatibility_level < 90) + BEGIN + SET @CheckUserDatabaseObjects = 0; + PRINT 'Databases with compatibility level < 90 found, so setting @CheckUserDatabaseObjects = 0.'; + PRINT 'The database-level checks rely on CTEs, which are not supported in SQL 2000 compat level databases.'; + PRINT 'Get with the cool kids and switch to a current compatibility level, Grandpa. To find the problems, run:'; + PRINT 'SELECT * FROM sys.databases WHERE compatibility_level < 90;'; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 204 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + '@CheckUserDatabaseObjects Disabled' AS Finding , + 'https://www.BrentOzar.com/blitz/' AS URL , + 'Since you have databases with compatibility_level < 90, we can''t run @CheckUserDatabaseObjects = 1. To find them: SELECT * FROM sys.databases WHERE compatibility_level < 90' AS Details; + END; - - IF @Debug = 1 RAISERROR('Waiting for 1 minute', 0, 1) WITH NOWAIT; - - WAITFOR DELAY '00:01:00.000'; + /* --TOURSTOP08-- */ + /* If the server is Amazon RDS, skip checks that it doesn't allow */ + IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND db_id('rdsadmin') IS NOT NULL + AND EXISTS(SELECT * FROM master.sys.all_objects WHERE name IN ('rds_startup_tasks', 'rds_help_revlogin', 'rds_hexadecimal', 'rds_failover_tracking', 'rds_database_tracking', 'rds_track_change')) + BEGIN + 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 (29); /* tables in model database created by users - not allowed */ + INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file in RDS */ + INSERT INTO #SkipChecks (CheckID) VALUES (62); /* Database compatibility level - cannot change in RDS */ + INSERT INTO #SkipChecks (CheckID) VALUES (68); /*Check for the last good DBCC CHECKDB date - can't run DBCC DBINFO() */ + INSERT INTO #SkipChecks (CheckID) VALUES (69); /* High VLF check - requires DBCC LOGINFO permission */ + INSERT INTO #SkipChecks (CheckID) VALUES (73); /* No Failsafe Operator Configured check */ + INSERT INTO #SkipChecks (CheckID) VALUES (92); /* Drive info check - requires xp_Fixeddrives permission */ + INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled */ + INSERT INTO #SkipChecks (CheckID) VALUES (177); /* Disabled Internal Monitoring Features check - requires dm_server_registry access */ + INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans checks - Maint plans not available in RDS*/ + INSERT INTO #SkipChecks (CheckID) VALUES (181); /*Find repetitive maintenance tasks*/ + + -- can check errorlog using rdsadmin.dbo.rds_read_error_log, so allow this check + --INSERT INTO #SkipChecks (CheckID) VALUES (193); /* xp_readerrorlog checking for IFI */ + + INSERT INTO #SkipChecks (CheckID) VALUES (211); /* xp_regread not allowed - checking for power saving */ + INSERT INTO #SkipChecks (CheckID) VALUES (212); /* xp_regread not allowed - checking for additional instances */ + INSERT INTO #SkipChecks (CheckID) VALUES (2301); /* sp_validatelogins called by Invalid login defined with Windows Authentication */ + + -- Following are skipped due to limited permissions in msdb/SQLAgent in RDS + INSERT INTO #SkipChecks (CheckID) VALUES (30); /* SQL Server Agent alerts not configured */ + INSERT INTO #SkipChecks (CheckID) VALUES (31); /* check whether we have NO ENABLED operators */ + INSERT INTO #SkipChecks (CheckID) VALUES (57); /* SQL Agent Job Runs at Startup */ + INSERT INTO #SkipChecks (CheckID) VALUES (59); /* Alerts Configured without Follow Up */ + INSERT INTO #SkipChecks (CheckID) VALUES (61); /*SQL Server Agent alerts do not exist for severity levels 19 through 25*/ + INSERT INTO #SkipChecks (CheckID) VALUES (79); /* Shrink Database Job check */ + INSERT INTO #SkipChecks (CheckID) VALUES (94); /* job failure without operator notification check */ + INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ + INSERT INTO #SkipChecks (CheckID) VALUES (98); /* check for disabled alerts */ + INSERT INTO #SkipChecks (CheckID) VALUES (123); /* Agent Jobs Starting Simultaneously */ + INSERT INTO #SkipChecks (CheckID) VALUES (219); /* check for alerts that do NOT include event descriptions in their outputs via email/pager/net-send */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://aws.amazon.com/rds/sqlserver/' AS URL , + 'Amazon RDS detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; + END; /* Amazon RDS skipped checks */ - END TRY + /* If the server is ExpressEdition, skip checks that it doesn't allow */ + IF CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%' + BEGIN + INSERT INTO #SkipChecks (CheckID) VALUES (30); /* Alerts not configured */ + INSERT INTO #SkipChecks (CheckID) VALUES (31); /* Operators not configured */ + INSERT INTO #SkipChecks (CheckID) VALUES (61); /* Agent alerts 19-25 */ + INSERT INTO #SkipChecks (CheckID) VALUES (73); /* Failsafe operator */ + INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://stackoverflow.com/questions/1169634/limitations-of-sql-server-express' AS URL , + 'Express Edition detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; + END; /* Express Edition skipped checks */ - BEGIN CATCH + /* If the server is an Azure Managed Instance, skip checks that it doesn't allow */ + IF SERVERPROPERTY('EngineEdition') = 8 + BEGIN + INSERT INTO #SkipChecks (CheckID) VALUES (1); /* Full backups - because of the MI GUID name bug mentioned here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ + INSERT INTO #SkipChecks (CheckID) VALUES (2); /* Log backups - because of the MI GUID name bug mentioned here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ + 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 */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'model'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'msdb'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'tempdb'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID) VALUES (224); /* CheckID 224 - Performance - SSRS/SSAS/SSIS Installed */ + INSERT INTO #SkipChecks (CheckID) VALUES (92); /* CheckID 92 - drive space */ + INSERT INTO #SkipChecks (CheckID) VALUES (258);/* CheckID 258 - Security - SQL Server service is running as LocalSystem or NT AUTHORITY\SYSTEM */ + INSERT INTO #SkipChecks (CheckID) VALUES (259);/* CheckID 259 - Security - SQL Server Agent service is running as LocalSystem or NT AUTHORITY\SYSTEM */ + INSERT INTO #SkipChecks (CheckID) VALUES (260); /* CheckID 260 - Security - SQL Server service account is member of Administrators */ + INSERT INTO #SkipChecks (CheckID) VALUES (261); /*CheckID 261 - Security - SQL Server Agent service account is member of Administrators */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://docs.microsoft.com/en-us/azure/sql-database/sql-database-managed-instance-index' AS URL , + 'Managed Instance detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; + END; /* Azure Managed Instance skipped checks */ + /* + That's the end of the SkipChecks stuff. + The next several tables are used by various checks later. + */ + IF OBJECT_ID('tempdb..#ConfigurationDefaults') IS NOT NULL + DROP TABLE #ConfigurationDefaults; + CREATE TABLE #ConfigurationDefaults + ( + name NVARCHAR(128) , + DefaultValue BIGINT, + CheckID INT + ); - SELECT @msg = N'Error inserting databases to msdbCentral.dbo.backup_worker, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL + DROP TABLE #Recompile; + CREATE TABLE #Recompile( + DBName varchar(200), + ProcName varchar(300), + RecompileFlag varchar(1), + SPSchema varchar(50) + ); - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - - END CATCH; - - - END; + IF OBJECT_ID('tempdb..#DatabaseDefaults') IS NOT NULL + DROP TABLE #DatabaseDefaults; + CREATE TABLE #DatabaseDefaults + ( + name NVARCHAR(128) , + DefaultValue NVARCHAR(200), + CheckID INT, + Priority INT, + Finding VARCHAR(200), + URL VARCHAR(200), + Details NVARCHAR(4000) + ); - /* Check to make sure job is still enabled */ - IF NOT EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name = 'sp_AllNightLog_PollForNewDatabases' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_PollForNewDatabases job is disabled, so gracefully exiting. It feels graceful to me, anyway.', 0, 1) WITH NOWAIT; - RETURN; - END - - END;-- End Pollster loop - - ELSE - - BEGIN - - RAISERROR('msdbCentral.dbo.backup_worker does not exist, please create it.', 0, 1) WITH NOWAIT; - RETURN; - - END; - RETURN; + IF OBJECT_ID('tempdb..#DatabaseScopedConfigurationDefaults') IS NOT NULL + DROP TABLE #DatabaseScopedConfigurationDefaults; + CREATE TABLE #DatabaseScopedConfigurationDefaults + (ID INT IDENTITY(1,1), configuration_id INT, [name] NVARCHAR(60), default_value sql_variant, default_value_for_secondary sql_variant, CheckID INT, ); + IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL + DROP TABLE #DBCCs; + CREATE TABLE #DBCCs + ( + ID INT IDENTITY(1, 1) + PRIMARY KEY , + ParentObject VARCHAR(255) , + Object VARCHAR(255) , + Field VARCHAR(255) , + Value VARCHAR(255) , + DbName NVARCHAR(128) NULL + ); -/* + IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL + DROP TABLE #LogInfo2012; + CREATE TABLE #LogInfo2012 + ( + recoveryunitid INT , + FileID SMALLINT , + FileSize BIGINT , + StartOffset BIGINT , + FSeqNo BIGINT , + [Status] TINYINT , + Parity TINYINT , + CreateLSN NUMERIC(38) + ); -End of Pollster + IF OBJECT_ID('tempdb..#LogInfo') IS NOT NULL + DROP TABLE #LogInfo; + CREATE TABLE #LogInfo + ( + FileID SMALLINT , + FileSize BIGINT , + StartOffset BIGINT , + FSeqNo BIGINT , + [Status] TINYINT , + Parity TINYINT , + CreateLSN NUMERIC(38) + ); -*/ + IF OBJECT_ID('tempdb..#partdb') IS NOT NULL + DROP TABLE #partdb; + CREATE TABLE #partdb + ( + dbname NVARCHAR(128) , + objectname NVARCHAR(200) , + type_desc NVARCHAR(128) + ); + IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL + DROP TABLE #TraceStatus; + CREATE TABLE #TraceStatus + ( + TraceFlag VARCHAR(10) , + status BIT , + Global BIT , + Session BIT + ); -/* + IF OBJECT_ID('tempdb..#driveInfo') IS NOT NULL + DROP TABLE #driveInfo; + CREATE TABLE #driveInfo + ( + drive NVARCHAR(2), + logical_volume_name NVARCHAR(36), --Limit is 32 for NTFS, 11 for FAT + available_MB DECIMAL(18, 0), + total_MB DECIMAL(18, 0), + used_percent DECIMAL(18, 2) + ); -Begin DiskPollster + IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL + DROP TABLE #dm_exec_query_stats; + CREATE TABLE #dm_exec_query_stats + ( + [id] [int] NOT NULL + IDENTITY(1, 1) , + [sql_handle] [varbinary](64) NOT NULL , + [statement_start_offset] [int] NOT NULL , + [statement_end_offset] [int] NOT NULL , + [plan_generation_num] [bigint] NOT NULL , + [plan_handle] [varbinary](64) NOT NULL , + [creation_time] [datetime] NOT NULL , + [last_execution_time] [datetime] NOT NULL , + [execution_count] [bigint] NOT NULL , + [total_worker_time] [bigint] NOT NULL , + [last_worker_time] [bigint] NOT NULL , + [min_worker_time] [bigint] NOT NULL , + [max_worker_time] [bigint] NOT NULL , + [total_physical_reads] [bigint] NOT NULL , + [last_physical_reads] [bigint] NOT NULL , + [min_physical_reads] [bigint] NOT NULL , + [max_physical_reads] [bigint] NOT NULL , + [total_logical_writes] [bigint] NOT NULL , + [last_logical_writes] [bigint] NOT NULL , + [min_logical_writes] [bigint] NOT NULL , + [max_logical_writes] [bigint] NOT NULL , + [total_logical_reads] [bigint] NOT NULL , + [last_logical_reads] [bigint] NOT NULL , + [min_logical_reads] [bigint] NOT NULL , + [max_logical_reads] [bigint] NOT NULL , + [total_clr_time] [bigint] NOT NULL , + [last_clr_time] [bigint] NOT NULL , + [min_clr_time] [bigint] NOT NULL , + [max_clr_time] [bigint] NOT NULL , + [total_elapsed_time] [bigint] NOT NULL , + [last_elapsed_time] [bigint] NOT NULL , + [min_elapsed_time] [bigint] NOT NULL , + [max_elapsed_time] [bigint] NOT NULL , + [query_hash] [binary](8) NULL , + [query_plan_hash] [binary](8) NULL , + [query_plan] [xml] NULL , + [query_plan_filtered] [nvarchar](MAX) NULL , + [text] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS + NULL , + [text_filtered] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS + NULL + ); -*/ + IF OBJECT_ID('tempdb..#ErrorLog') IS NOT NULL + DROP TABLE #ErrorLog; + CREATE TABLE #ErrorLog + ( + LogDate DATETIME , + ProcessInfo NVARCHAR(20) , + [Text] NVARCHAR(1000) + ); + IF OBJECT_ID('tempdb..#fnTraceGettable') IS NOT NULL + DROP TABLE #fnTraceGettable; + CREATE TABLE #fnTraceGettable + ( + TextData NVARCHAR(4000) , + DatabaseName NVARCHAR(256) , + EventClass INT , + Severity INT , + StartTime DATETIME , + EndTime DATETIME , + Duration BIGINT , + NTUserName NVARCHAR(256) , + NTDomainName NVARCHAR(256) , + HostName NVARCHAR(256) , + ApplicationName NVARCHAR(256) , + LoginName NVARCHAR(256) , + DBUserName NVARCHAR(256) + ); -/* + IF OBJECT_ID('tempdb..#Instances') IS NOT NULL + DROP TABLE #Instances; + CREATE TABLE #Instances + ( + Instance_Number NVARCHAR(MAX) , + Instance_Name NVARCHAR(MAX) , + Data_Field NVARCHAR(MAX) + ); -This section runs in a loop checking restore path for new databases added to the server, or broken restores + IF OBJECT_ID('tempdb..#IgnorableWaits') IS NOT NULL + DROP TABLE #IgnorableWaits; + CREATE TABLE #IgnorableWaits (wait_type NVARCHAR(60)); + INSERT INTO #IgnorableWaits VALUES ('BROKER_EVENTHANDLER'); + INSERT INTO #IgnorableWaits VALUES ('BROKER_RECEIVE_WAITFOR'); + INSERT INTO #IgnorableWaits VALUES ('BROKER_TASK_STOP'); + INSERT INTO #IgnorableWaits VALUES ('BROKER_TO_FLUSH'); + INSERT INTO #IgnorableWaits VALUES ('BROKER_TRANSMITTER'); + INSERT INTO #IgnorableWaits VALUES ('CHECKPOINT_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('CLR_AUTO_EVENT'); + INSERT INTO #IgnorableWaits VALUES ('CLR_MANUAL_EVENT'); + INSERT INTO #IgnorableWaits VALUES ('CLR_SEMAPHORE'); + INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_EVENT'); + INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_MUTEX'); + INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_EVENTS_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_WORKER_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('DBMIRRORING_CMD'); + INSERT INTO #IgnorableWaits VALUES ('DIRTY_PAGE_POLL'); + INSERT INTO #IgnorableWaits VALUES ('DISPATCHER_QUEUE_SEMAPHORE'); + INSERT INTO #IgnorableWaits VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT'); + INSERT INTO #IgnorableWaits VALUES ('FT_IFTSHC_MUTEX'); + INSERT INTO #IgnorableWaits VALUES ('HADR_CLUSAPI_CALL'); + INSERT INTO #IgnorableWaits VALUES ('HADR_FABRIC_CALLBACK'); + INSERT INTO #IgnorableWaits VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION'); + INSERT INTO #IgnorableWaits VALUES ('HADR_LOGCAPTURE_WAIT'); + INSERT INTO #IgnorableWaits VALUES ('HADR_NOTIFICATION_DEQUEUE'); + INSERT INTO #IgnorableWaits VALUES ('HADR_TIMER_TASK'); + INSERT INTO #IgnorableWaits VALUES ('HADR_WORK_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('LAZYWRITER_SLEEP'); + INSERT INTO #IgnorableWaits VALUES ('LOGMGR_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('ONDEMAND_TASK_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_DRAIN_WORKER'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_LOG_CACHE'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_TRAN_LIST'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_WORKER_SYNC'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_WORKER_WAIT_WORK'); + INSERT INTO #IgnorableWaits VALUES ('POPULATE_LOCK_ORDINALS'); + INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM'); + INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS'); + INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS'); + INSERT INTO #IgnorableWaits VALUES ('PVS_PREALLOCATE'); + INSERT INTO #IgnorableWaits VALUES ('PWAIT_EXTENSIBILITY_CLEANUP_TASK'); + INSERT INTO #IgnorableWaits VALUES ('QDS_ASYNC_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'); + INSERT INTO #IgnorableWaits VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'); + INSERT INTO #IgnorableWaits VALUES ('QDS_SHUTDOWN_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('REDO_THREAD_PENDING_WORK'); + INSERT INTO #IgnorableWaits VALUES ('REQUEST_FOR_DEADLOCK_SEARCH'); + INSERT INTO #IgnorableWaits VALUES ('SLEEP_SYSTEMTASK'); + INSERT INTO #IgnorableWaits VALUES ('SLEEP_TASK'); + INSERT INTO #IgnorableWaits VALUES ('SOS_WORK_DISPATCHER'); + INSERT INTO #IgnorableWaits VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP'); + INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_BUFFER_FLUSH'); + INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'); + INSERT INTO #IgnorableWaits VALUES ('UCS_SESSION_REGISTRATION'); + INSERT INTO #IgnorableWaits VALUES ('VDI_CLIENT_OTHER'); + INSERT INTO #IgnorableWaits VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'); + INSERT INTO #IgnorableWaits VALUES ('WAITFOR'); + INSERT INTO #IgnorableWaits VALUES ('XE_DISPATCHER_WAIT'); + INSERT INTO #IgnorableWaits VALUES ('XE_LIVE_TARGET_TVF'); + INSERT INTO #IgnorableWaits VALUES ('XE_TIMER_EVENT'); -*/ + IF @Debug IN (1, 2) RAISERROR('Setting @MsSinceWaitsCleared', 0, 1) WITH NOWAIT; -DiskPollster: + SELECT @MsSinceWaitsCleared = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) * 60000.0 + FROM sys.databases + WHERE name = 'tempdb'; - IF @Debug = 1 RAISERROR('Beginning DiskPollster', 0, 1) WITH NOWAIT; - - IF OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL - - BEGIN - - WHILE @PollDiskForNewDatabases = 1 - + /* Have they cleared wait stats? Using a 10% fudge factor */ + IF @MsSinceWaitsCleared * .9 > (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')) BEGIN - BEGIN TRY - - IF @Debug = 1 RAISERROR('Checking for new databases in: ', 0, 1) WITH NOWAIT; - IF @Debug = 1 RAISERROR(@restore_path_base, 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 185) WITH NOWAIT; - /* - - Look for new non-system databases -- there should probably be additional filters here for accessibility, etc. + SET @MsSinceWaitsCleared = (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')); + IF @MsSinceWaitsCleared = 0 SET @MsSinceWaitsCleared = 1; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES( 185, + 240, + 'Wait Stats', + 'Wait Stats Have Been Cleared', + 'https://www.brentozar.com/go/waits', + 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + + CONVERT(NVARCHAR(100), + DATEADD(MINUTE, (-1. * (@MsSinceWaitsCleared) / 1000. / 60.), GETDATE()), 120)); + END; - */ - - /* - - This setups up the @cmd variable to check the restore path for new folders - - In our case, a new folder means a new database, because we assume a pristine path + /* @CpuMsSinceWaitsCleared is used for waits stats calculations */ + + IF @Debug IN (1, 2) RAISERROR('Setting @CpuMsSinceWaitsCleared', 0, 1) WITH NOWAIT; + + SELECT @CpuMsSinceWaitsCleared = @MsSinceWaitsCleared * scheduler_count + FROM sys.dm_os_sys_info; - */ + /* If we're outputting CSV or Markdown, don't bother checking the plan cache because we cannot export plans. */ + IF @OutputType = 'CSV' OR @OutputType = 'MARKDOWN' + SET @CheckProcedureCache = 0; - SET @cmd = N'DIR /b "' + @restore_path_base + N'"'; - - IF @Debug = 1 - BEGIN - PRINT @cmd; - END - - - DELETE @FileList; - INSERT INTO @FileList (BackupFile) - EXEC master.sys.xp_cmdshell @cmd; - - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 + /* If we're posting a question on Stack, include background info on the server */ + IF @OutputType = 'MARKDOWN' + SET @CheckServerInfo = 1; - BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; + /* Only run CheckUserDatabaseObjects if there are less than 50 databases. */ + IF @BringThePain = 0 AND 50 <= (SELECT COUNT(*) FROM sys.databases) AND @CheckUserDatabaseObjects = 1 + BEGIN + SET @CheckUserDatabaseObjects = 0; + PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary problems for the server and/or user.'; + PRINT 'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.'; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 201 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + '@CheckUserDatabaseObjects Disabled' AS Finding , + 'https://www.BrentOzar.com/blitz/' AS URL , + 'If you want to check 50+ databases, you have to also use @BringThePain = 1.' AS Details; + END; - END; + /* Sanitize our inputs */ + SELECT + @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 + /* Get the major and minor build numbers */ + + IF @Debug IN (1, 2) RAISERROR('Getting version information.', 0, 1) WITH NOWAIT; + + SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); + SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), + @ProductVersionMinor = PARSENAME(CONVERT(varchar(32), @ProductVersion), 2); + + /* + Whew! we're finally done with the setup, and we can start doing checks. + First, let's make sure we're actually supposed to do checks on this server. + The user could have passed in a SkipChecks table that specified to skip ALL + checks on this server, so let's check for that: + */ + IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID IS NULL ) ) + OR ( @SkipChecksTable IS NULL ) + ) + BEGIN - BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @restore_path_base) WITH NOWAIT; + /* + Extract DBCC DBINFO data from the server. This data is used for check 2 using + the dbi_LastLogBackupTime field and check 68 using the dbi_LastKnownGood field. + NB: DBCC DBINFO is not available on AWS RDS databases so if the server is RDS + (which will have previously triggered inserting a checkID 223 record) and at + least one of the relevant checks is not being skipped then we can extract the + dbinfo information. + */ + IF NOT EXISTS + ( + SELECT 1/0 + FROM #BlitzResults + WHERE CheckID = 223 AND URL = 'https://aws.amazon.com/rds/sqlserver/' + ) AND NOT EXISTS + ( + SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID IN (2, 68) + ) + BEGIN - END; + IF @Debug IN (1, 2) RAISERROR('Extracting DBCC DBINFO data (used in checks 2 and 68).', 0, 1, 68) WITH NOWAIT; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 + EXEC sp_MSforeachdb N'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT #DBCCs + (ParentObject, + Object, + Field, + Value) + EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); + UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; + END - BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END + /* + Our very first check! We'll put more comments in this one just to + explain exactly how it works. First, we check to see if we're + supposed to skip CheckID 1 (that's the check we're working on.) + */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 1 ) + BEGIN - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The user name or password is incorrect.' - ) = 1 + /* + Below, we check master.sys.databases looking for databases + that haven't had a backup in the last week. If we find any, + we insert them into #BlitzResults, the temp table that + tracks our server's problems. Note that if the check does + NOT find any problems, we don't save that. We're only + saving the problems, not the successful checks. + */ - BEGIN - - RAISERROR('Incorrect user name or password for %s', 16, 1, @restore_path_base) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 1) WITH NOWAIT; - END; + IF SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances need a special query */ + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 1 AS CheckID , + d.[name] AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backups Not Performed Recently' AS Finding , + 'https://www.brentozar.com/go/nobak' AS URL , + 'Last backed up: ' + + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details + FROM master.sys.databases d + LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'D' + AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ + WHERE d.database_id <> 2 /* Bonus points if you know what that means */ + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 1) + /* + The above NOT IN filters out the databases we're not supposed to check. + */ + GROUP BY d.name + HAVING MAX(b.backup_finish_date) <= DATEADD(dd, + -7, GETDATE()) + OR MAX(b.backup_finish_date) IS NULL; + END; + + ELSE /* SERVERPROPERTY('EngineName') must be 8, Azure Managed Instances */ + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 1 AS CheckID , + d.[name] AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backups Not Performed Recently' AS Finding , + 'https://www.brentozar.com/go/nobak' AS URL , + 'Last backed up: ' + + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details + FROM master.sys.databases d + LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'D' + WHERE d.database_id <> 2 /* Bonus points if you know what that means */ + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 1) + /* + The above NOT IN filters out the databases we're not supposed to check. + */ + GROUP BY d.name + HAVING MAX(b.backup_finish_date) <= DATEADD(dd, + -7, GETDATE()) + OR MAX(b.backup_finish_date) IS NULL; + END; - INSERT msdb.dbo.restore_worker (database_name) - SELECT fl.BackupFile - FROM @FileList AS fl - WHERE fl.BackupFile IS NOT NULL - AND NOT EXISTS - ( - SELECT 1 - FROM msdb.dbo.restore_worker rw - WHERE rw.database_name = fl.BackupFile - ) - IF @Debug = 1 RAISERROR('Checking for wayward databases', 0, 1) WITH NOWAIT; /* - - This section aims to find databases that have - * Had a log restore ever (the default for finish time is 9999-12-31, so anything with a more recent finish time has had a log restore) - * Not had a log restore start in the last 5 minutes (this could be trouble! or a really big log restore) - * Also checks msdb.dbo.backupset to make sure the database has a full backup associated with it (otherwise it's the first full, and we don't need to start adding log restores yet) - + And there you have it. The rest of this stored procedure works the same + way: it asks: + - Should I skip this check? + - If not, do I find problems? + - Insert the results into #BlitzResults */ - - IF EXISTS ( - - SELECT 1 - FROM msdb.dbo.restore_worker rw WITH (READPAST) - WHERE rw.last_log_restore_finish_time < '99991231' - AND rw.last_log_restore_start_time < DATEADD(MINUTE, -5, GETDATE()) - AND EXISTS ( - SELECT 1 - FROM msdb.dbo.restorehistory r - WHERE r.destination_database_name = rw.database_name - AND r.restore_type = 'D' - ) - ) - - BEGIN - - IF @Debug = 1 RAISERROR('Resetting databases with a log restore and no log restore in the last 5 minutes', 0, 1) WITH NOWAIT; - - UPDATE rw - SET rw.is_started = 0, - rw.is_completed = 1, - rw.last_log_restore_start_time = '19000101' - FROM msdb.dbo.restore_worker rw - WHERE rw.last_log_restore_finish_time < '99991231' - AND rw.last_log_restore_start_time < DATEADD(MINUTE, -5, GETDATE()) - AND EXISTS ( - SELECT 1 - FROM msdb.dbo.restorehistory r - WHERE r.destination_database_name = rw.database_name - AND r.restore_type = 'D' - ); + END; - - END; --End check for wayward databases + /* + And that's the end of CheckID #1. - /* - - Wait 1 minute between runs, we don't need to be checking this constantly - - */ + CheckID #2 is a little simpler because it only involves one query, and it's + more typical for queries that people contribute. But keep reading, because + the next check gets more complex again. + */ - /* Check to make sure job is still enabled */ - IF NOT EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name = 'sp_AllNightLog_PollDiskForNewDatabases' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_PollDiskForNewDatabases job is disabled, so gracefully exiting. It feels graceful to me, anyway.', 0, 1) WITH NOWAIT; - RETURN; - END - - IF @Debug = 1 RAISERROR('Waiting for 1 minute', 0, 1) WITH NOWAIT; - - WAITFOR DELAY '00:01:00.000'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 2 ) + BEGIN - END TRY + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; - BEGIN CATCH + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 2 AS CheckID , + d.name AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Full Recovery Model w/o Log Backups' AS Finding , + 'https://www.brentozar.com/go/biglogs' AS URL , + ( 'The ' + CAST(CAST((SELECT ((SUM([mf].[size]) * 8.) / 1024.) FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = d.[database_id] AND [mf].[type_desc] = 'LOG') AS DECIMAL(18,2)) AS VARCHAR(30)) + 'MB log file has not been backed up in the last week.' ) AS Details + FROM master.sys.databases d + LEFT JOIN #DBCCs ll On ll.DbName = d.name And ll.Field = 'dbi_LastLogBackupTime' + WHERE d.recovery_model IN ( 1, 2 ) + AND d.database_id NOT IN ( 2, 3 ) + AND d.source_database_id IS NULL + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 2) + AND ( + ( + /* We couldn't get a value from the DBCC DBINFO data so let's check the msdb backup history information */ + [ll].[Value] Is Null + AND NOT EXISTS ( SELECT * + FROM msdb.dbo.backupset b + WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd,-7, GETDATE()) + ) + ) + OR + ( + Convert(datetime,ll.Value,21) < DATEADD(dd,-7, GETDATE()) + ) + ); + END; - SELECT @msg = N'Error inserting databases to msdb.dbo.restore_worker, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + /* + CheckID #256 is searching for backups to NUL. + */ - - WHILE @@TRANCOUNT > 0 - ROLLBACK; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 256 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; - END CATCH; - - - END; - - END;-- End Pollster loop - - ELSE - - BEGIN - - RAISERROR('msdb.dbo.restore_worker does not exist, please create it.', 0, 1) WITH NOWAIT; - RETURN; - - END; - RETURN; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 256 AS CheckID , + d.name AS DatabaseName, + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Log Backups to NUL' AS Finding , + 'https://www.brentozar.com/go/nul' AS URL , + N'The transaction log file has been backed up ' + CAST((SELECT count(*) + FROM msdb.dbo.backupset AS b INNER JOIN + msdb.dbo.backupmediafamily AS bmf + ON b.media_set_id = bmf.media_set_id + WHERE b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS = d.name COLLATE SQL_Latin1_General_CP1_CI_AS + AND bmf.physical_device_name = 'NUL' + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd, + -7, GETDATE())) AS NVARCHAR(8)) + ' time(s) to ''NUL'' in the last week, which means the backup does not exist. This breaks point-in-time recovery.' AS Details + FROM master.sys.databases AS d + WHERE d.recovery_model IN ( 1, 2 ) + AND d.database_id NOT IN ( 2, 3 ) + AND d.source_database_id IS NULL + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + --AND d.name NOT IN ( SELECT DISTINCT + -- DatabaseName + -- FROM #SkipChecks + -- WHERE CheckID IS NULL OR CheckID = 2) + AND EXISTS ( SELECT * + FROM msdb.dbo.backupset AS b INNER JOIN + msdb.dbo.backupmediafamily AS bmf + ON b.media_set_id = bmf.media_set_id + WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND bmf.physical_device_name = 'NUL' + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd, + -7, GETDATE()) ); + END; + /* + Next up, we've got CheckID 8. (These don't have to go in order.) This one + won't work on SQL Server 2005 because it relies on a new DMV that didn't + exist prior to SQL Server 2008. This means we have to check the SQL Server + version first, then build a dynamic string with the query we want to run: + */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 8 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN -/* + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 8) WITH NOWAIT; -Begin LogShamer + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, Priority, + FindingsGroup, + Finding, URL, + Details) + SELECT 8 AS CheckID, + 230 AS Priority, + ''Security'' AS FindingsGroup, + ''Server Audits Running'' AS Finding, + ''https://www.brentozar.com/go/audits'' AS URL, + (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status 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.'; + + EXECUTE(@StringToExecute); + END; + END; -*/ - -LogShamer: - - IF @Debug = 1 RAISERROR('Beginning Backups', 0, 1) WITH NOWAIT; - - IF OBJECT_ID('msdbCentral.dbo.backup_worker') IS NOT NULL - - BEGIN - - /* - - Make sure configuration table exists... - - */ - - IF OBJECT_ID('msdbCentral.dbo.backup_configuration') IS NOT NULL - - BEGIN - - IF @Debug = 1 RAISERROR('Checking variables', 0, 1) WITH NOWAIT; - - /* - - These settings are configurable - - I haven't found a good way to find the default backup path that doesn't involve xp_regread - - */ - - SELECT @rpo = CONVERT(INT, configuration_setting) - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'log backup frequency' - AND database_name = N'all'; - - - IF @rpo IS NULL - BEGIN - RAISERROR('@rpo cannot be NULL. Please check the msdbCentral.dbo.backup_configuration table', 0, 1) WITH NOWAIT; - RETURN; - END; - - - SELECT @backup_path = CONVERT(NVARCHAR(512), configuration_setting) - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'log backup path' - AND database_name = N'all'; - - - IF @backup_path IS NULL - BEGIN - RAISERROR('@backup_path cannot be NULL. Please check the msdbCentral.dbo.backup_configuration table', 0, 1) WITH NOWAIT; - RETURN; - END; + /* + But what if you need to run a query in every individual database? + Hop down to the @CheckUserDatabaseObjects section. - SELECT @changebackuptype = configuration_setting - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'change backup type' - AND database_name = N'all'; + And that's the basic idea! You can read through the rest of the + checks if you like - some more exciting stuff happens closer to the + end of the stored proc, where we start doing things like checking + the plan cache, but those aren't as cleanly commented. - SELECT @encrypt = configuration_setting - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'encrypt' - AND database_name = N'all'; + To contribute your own checks or fix bugs, learn more here: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/main/CONTRIBUTING.md + */ - SELECT @encryptionalgorithm = configuration_setting - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'encryptionalgorithm' - AND database_name = N'all'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 93 ) + BEGIN - SELECT @servercertificate = configuration_setting - FROM msdbCentral.dbo.backup_configuration c - WHERE configuration_name = N'servercertificate' - AND database_name = N'all'; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 93) WITH NOWAIT; - IF @encrypt = N'Y' AND (@encryptionalgorithm IS NULL OR @servercertificate IS NULL) - BEGIN - RAISERROR('If encryption is Y, then both the encryptionalgorithm and servercertificate must be set. Please check the msdbCentral.dbo.backup_configuration table', 0, 1) WITH NOWAIT; - RETURN; - END; - - END; - - ELSE - - BEGIN - - RAISERROR('msdbCentral.dbo.backup_configuration does not exist, please run setup script', 0, 1) WITH NOWAIT; - RETURN; - - END; - - - WHILE @Backup = 1 + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 93 AS CheckID , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backing Up to Same Drive Where Databases Reside' AS Finding , + 'https://www.brentozar.com/go/backup' AS URL , + CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' + + UPPER(LEFT(bmf.physical_device_name, 3)) + + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details + FROM msdb.dbo.backupmediafamily AS bmf + INNER JOIN msdb.dbo.backupset AS bs ON bmf.media_set_id = bs.media_set_id + AND bs.backup_start_date >= ( DATEADD(dd, + -14, GETDATE()) ) + /* Filter out databases that were recently restored: */ + LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name AND rh.restore_date > DATEADD(dd, -14, GETDATE()) + WHERE UPPER(LEFT(bmf.physical_device_name, 3)) <> 'HTT' AND + bmf.physical_device_name NOT LIKE '\\%' AND -- GitHub Issue #2141 + @IsWindowsOperatingSystem = 1 AND -- GitHub Issue #1995 + UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( + SELECT DISTINCT + UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) + FROM sys.master_files AS mf + WHERE mf.database_id <> 2 ) + AND rh.destination_database_name IS NULL + GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)); + END; - /* - - Start loop to take log backups + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 119 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_database_encryption_keys' ) + BEGIN - */ + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 119) WITH NOWAIT; - - BEGIN - - BEGIN TRY + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, DatabaseName, URL, Details) + SELECT 119 AS CheckID, + 1 AS Priority, + ''Backup'' AS FindingsGroup, + ''TDE Certificate Not Backed Up Recently'' AS Finding, + db_name(dek.database_id) AS DatabaseName, + ''https://www.brentozar.com/go/tde'' AS URL, + ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details + FROM master.sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint + WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; - BEGIN TRAN; - - IF @Debug = 1 RAISERROR('Begin tran to grab a database to back up', 0, 1) WITH NOWAIT; + 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.'; + + EXECUTE(@StringToExecute); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 202 ) + AND EXISTS ( SELECT * + FROM sys.all_columns c + WHERE c.name = 'pvt_key_last_backup_date' ) + AND EXISTS ( SELECT * + FROM msdb.INFORMATION_SCHEMA.COLUMNS c + WHERE c.TABLE_NAME = 'backupset' AND c.COLUMN_NAME = 'encryptor_thumbprint' ) + BEGIN - /* - - This grabs a database for a worker to work on + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT; - The locking hints hope to provide some isolation when 10+ workers are in action - - */ - + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 202 AS CheckID, + 1 AS Priority, + ''Backup'' AS FindingsGroup, + ''Encryption Certificate Not Backed Up Recently'' AS Finding, + ''https://www.brentozar.com/go/tde'' AS URL, + ''The certificate '' + c.name + '' is used to encrypt database backups. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details + FROM sys.certificates c + INNER JOIN msdb.dbo.backupset bs ON c.thumbprint = bs.encryptor_thumbprint + WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; - SELECT TOP (1) - @database = bw.database_name - FROM msdbCentral.dbo.backup_worker bw WITH (UPDLOCK, HOLDLOCK, ROWLOCK) - WHERE - ( /*This section works on databases already part of the backup cycle*/ - bw.is_started = 0 - AND bw.is_completed = 1 - AND bw.last_log_backup_start_time < DATEADD(SECOND, (@rpo * -1), GETDATE()) - AND (bw.error_number IS NULL OR bw.error_number > 0) /* negative numbers indicate human attention required */ - ) - OR - ( /*This section picks up newly added databases by Pollster*/ - bw.is_started = 0 - AND bw.is_completed = 0 - AND bw.last_log_backup_start_time = '1900-01-01 00:00:00.000' - AND bw.last_log_backup_finish_time = '9999-12-31 00:00:00.000' - AND (bw.error_number IS NULL OR bw.error_number > 0) /* negative numbers indicate human attention required */ - ) - ORDER BY bw.last_log_backup_start_time ASC, bw.last_log_backup_finish_time ASC, bw.database_name ASC; - - - IF @database IS NOT NULL - BEGIN - SET @msg = N'Updating backup_worker for database ' + ISNULL(@database, 'UH OH NULL @database'); - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - /* - - Update the worker table so other workers know a database is being backed up - - */ - - - UPDATE bw - SET bw.is_started = 1, - bw.is_completed = 0, - bw.last_log_backup_start_time = GETDATE() - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.database_name = @database; - END - - COMMIT; - - END TRY - - BEGIN CATCH - - /* - - Do I need to build retry logic in here? Try to catch deadlocks? I don't know yet! - - */ - - SELECT @msg = N'Error securing a database to backup, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + 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.'; + + EXECUTE(@StringToExecute); + END; - SET @database = NULL; - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - END CATCH; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 3 ) + BEGIN + IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY backup_start_date) + BEGIN - /* If we don't find a database to work on, wait for a few seconds */ - IF @database IS NULL + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 3) WITH NOWAIT; - BEGIN - IF @Debug = 1 RAISERROR('No databases to back up right now, starting 3 second throttle', 0, 1) WITH NOWAIT; - WAITFOR DELAY '00:00:03.000'; - - /* Check to make sure job is still enabled */ - IF NOT EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Backup%' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_Backup jobs are disabled, so gracefully exiting. It feels graceful to me, anyway.', 0, 1) WITH NOWAIT; - RETURN; - END + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 3 AS CheckID , + 'msdb' , + 200 AS Priority , + 'Backup' AS FindingsGroup , + 'MSDB Backup History Not Purged' AS Finding , + 'https://www.brentozar.com/go/history' AS URL , + ( 'Database backup history retained back to ' + + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details + FROM msdb.dbo.backupset bs + LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name + WHERE rh.destination_database_name IS NULL + ORDER BY bs.backup_start_date ASC; + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 186 ) + BEGIN + IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY backup_start_date) - END - - - BEGIN TRY - BEGIN - - IF @database IS NOT NULL - - /* - Make sure we have a database to work on -- I should make this more robust so we do something if it is NULL, maybe + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 186) WITH NOWAIT; - */ - - - BEGIN - - SET @msg = N'Taking backup of ' + ISNULL(@database, 'UH OH NULL @database'); - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - /* - - Call Ola's proc to backup the database - - */ - - IF @encrypt = 'Y' - EXEC master.dbo.DatabaseBackup @Databases = @database, --Database we're working on - @BackupType = 'LOG', --Going for the LOGs - @Directory = @backup_path, --The path we need to back up to - @Verify = 'N', --We don't want to verify these, it eats into job time - @ChangeBackupType = @changebackuptype, --If we need to switch to a FULL because one hasn't been taken - @CheckSum = 'Y', --These are a good idea - @Compress = 'Y', --This is usually a good idea - @LogToTable = 'Y', --We should do this for posterity - @Encrypt = @encrypt, - @EncryptionAlgorithm = @encryptionalgorithm, - @ServerCertificate = @servercertificate; - - ELSE - EXEC master.dbo.DatabaseBackup @Databases = @database, --Database we're working on - @BackupType = 'LOG', --Going for the LOGs - @Directory = @backup_path, --The path we need to back up to - @Verify = 'N', --We don't want to verify these, it eats into job time - @ChangeBackupType = @changebackuptype, --If we need to switch to a FULL because one hasn't been taken - @CheckSum = 'Y', --These are a good idea - @Compress = 'Y', --This is usually a good idea - @LogToTable = 'Y'; --We should do this for posterity - - - /* - - Catch any erroneous zones - - */ - - SELECT @error_number = ERROR_NUMBER(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - END; --End call to dbo.DatabaseBackup - - END; --End successful check of @database (not NULL) - - END TRY - - BEGIN CATCH - - IF @error_number IS NOT NULL + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 186 AS CheckID , + 'msdb' , + 200 AS Priority , + 'Backup' AS FindingsGroup , + 'MSDB Backup History Purged Too Frequently' AS Finding , + 'https://www.brentozar.com/go/history' AS URL , + ( 'Database backup history only retained back to ' + + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details + FROM msdb.dbo.backupset bs + ORDER BY backup_start_date ASC; + END; + END; - /* + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 178 ) + AND EXISTS (SELECT * + FROM msdb.dbo.backupset bs + WHERE bs.type = 'D' + AND bs.backup_size >= 50000000000 /* At least 50GB */ + AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ + AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */) + BEGIN - If the ERROR() function returns a number, update the table with it and the last error date. - - Also update the last start time to 1900-01-01 so it gets picked back up immediately -- the query to find a log backup to take sorts by start time - - */ - - BEGIN - - SET @msg = N'Error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()); - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; - - SET @msg = N'Updating backup_worker for database ' + ISNULL(@database, 'UH OH NULL @database') + ' for unsuccessful backup'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - - - UPDATE bw - SET bw.is_started = 0, - bw.is_completed = 1, - bw.last_log_backup_start_time = '19000101', - bw.error_number = @error_number, - bw.last_error_date = GETDATE() - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.database_name = @database; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 178) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 178 AS CheckID , + 200 AS Priority , + 'Performance' AS FindingsGroup , + 'Snapshot Backups Occurring' AS Finding , + 'https://www.brentozar.com/go/snaps' AS URL , + ( CAST(COUNT(*) AS VARCHAR(20)) + ' snapshot-looking backups have occurred in the last two weeks, indicating that IO may be freezing up.') AS Details + FROM msdb.dbo.backupset bs + WHERE bs.type = 'D' + AND bs.backup_size >= 50000000000 /* At least 50GB */ + AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ + AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()); /* In the last 2 weeks */ + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 236 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 236) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 236 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingsGroup , + 'Snapshotting Too Many Databases' AS Finding , + 'https://www.brentozar.com/go/toomanysnaps' AS URL , + ( CAST(SUM(1) AS VARCHAR(20)) + ' databases snapshotted at once in the last two weeks, indicating that IO may be freezing up. Microsoft does not recommend VSS snaps for 35 or more databases.') AS Details + FROM msdb.dbo.backupset bs + WHERE bs.type = 'D' + AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */ + GROUP BY bs.backup_finish_date + HAVING SUM(1) >= 35 + ORDER BY SUM(1) DESC; + END; - /* - - Set @database back to NULL to avoid variable assignment weirdness - - */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 4 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 4) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 4 AS CheckID , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Sysadmins' AS Finding , + 'https://www.brentozar.com/go/sa' AS URL , + ( 'Login [' + l.name + + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details + FROM master.sys.syslogins l + WHERE l.sysadmin = 1 + AND l.name <> SUSER_SNAME(0x01) + AND l.denylogin = 0 + AND l.name NOT LIKE 'NT SERVICE\%' + AND l.name <> 'l_certSignSmDetach'; /* Added in SQL 2016 */ + END; - SET @database = NULL; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE CheckID = 2301 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2301) WITH NOWAIT; + + /* + #InvalidLogins is filled at the start during the permissions check IF we are not sysadmin + filling it now if we are sysadmin + */ + IF @sa = 1 + BEGIN + INSERT INTO #InvalidLogins + ( + [LoginSID] + ,[LoginName] + ) + EXEC sp_validatelogins; + END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 2301 AS CheckID , + 230 AS Priority , + '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. Sometimes empty AD groups can show up here so check thoroughly.') AS Details + FROM #InvalidLogins + ; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 5 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 5) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 5 AS CheckID , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Security Admins' AS Finding , + 'https://www.brentozar.com/go/sa' AS URL , + ( 'Login [' + l.name + + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details + FROM master.sys.syslogins l + WHERE l.securityadmin = 1 + AND l.name <> SUSER_SNAME(0x01) + AND l.denylogin = 0; + END; - - /* - - Wait around for a second so we're not just spinning wheels -- this only runs if the BEGIN CATCH is triggered by an error + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 104 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 104) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] + ) + SELECT 104 AS [CheckID] , + 230 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Login Can Control Server' AS [Finding] , + 'https://www.brentozar.com/go/sa' AS [URL] , + 'Login [' + pri.[name] + + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] + FROM sys.server_principals AS pri + WHERE pri.[principal_id] IN ( + SELECT p.[grantee_principal_id] + FROM sys.server_permissions AS p + WHERE p.[state] IN ( 'G', 'W' ) + AND p.[class] = 100 + AND p.[type] = 'CL' ) + AND pri.[name] NOT LIKE '##%##'; + END; - */ - - IF @Debug = 1 RAISERROR('Starting 1 second throttle', 0, 1) WITH NOWAIT; - - WAITFOR DELAY '00:00:01.000'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 6 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; - END; -- End update of unsuccessful backup - - END CATCH; - - IF @database IS NOT NULL AND @error_number IS NULL + + IF @UsualOwnerOfJobs IS NULL + SET @UsualOwnerOfJobs = SUSER_SNAME(0x01); + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 6 AS CheckID , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Jobs Owned By Users' AS Finding , + 'https://www.brentozar.com/go/owners' AS URL , + ( 'Job [' + j.name + '] is owned by [' + + SUSER_SNAME(j.owner_sid) + + '] - 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) <> @UsualOwnerOfJobs; + END; - /* + /* --TOURSTOP06-- */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 7 ) + BEGIN + /* --TOURSTOP02-- */ - If no error, update everything normally + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 7) WITH NOWAIT; - */ - - - BEGIN - - IF @Debug = 1 RAISERROR('Error number IS NULL', 0, 1) WITH NOWAIT; - - SET @msg = N'Updating backup_worker for database ' + ISNULL(@database, 'UH OH NULL @database') + ' for successful backup'; - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - - UPDATE bw - SET bw.is_started = 0, - bw.is_completed = 1, - bw.last_log_backup_finish_time = GETDATE() - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.database_name = @database; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 7 AS CheckID , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Stored Procedure Runs at Startup' AS Finding , + 'https://www.brentozar.com/go/startup' AS URL , + ( 'Stored procedure [master].[' + + r.SPECIFIC_SCHEMA + '].[' + + r.SPECIFIC_NAME + + '] runs automatically when SQL Server starts up. Make sure you know exactly what this stored procedure is doing, because it could pose a security risk.' ) AS Details + FROM master.INFORMATION_SCHEMA.ROUTINES r + WHERE OBJECTPROPERTY(OBJECT_ID(ROUTINE_NAME), + 'ExecIsStartup') = 1; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 10 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN - /* - - Set @database back to NULL to avoid variable assignment weirdness + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 10) WITH NOWAIT; - */ - - SET @database = NULL; - - - END; -- End update for successful backup + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 10 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Resource Governor Enabled'' AS Finding, + ''https://www.brentozar.com/go/rg'' AS URL, + (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1 OPTION (RECOMPILE);'; - - END; -- End @Backup WHILE loop + 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.'; - - END; -- End successful check for backup_worker and subsequent code + EXECUTE(@StringToExecute); + END; + END; - - ELSE - - BEGIN - - RAISERROR('msdbCentral.dbo.backup_worker does not exist, please run setup script', 0, 1) WITH NOWAIT; - - RETURN; - - END; -RETURN; - - -/* - -Begin Restoregasm_Addict section - -*/ - -Restoregasm_Addict: - -IF @Restore = 1 - IF @Debug = 1 RAISERROR('Beginning Restores', 0, 1) WITH NOWAIT; - - /* Check to make sure backup jobs aren't enabled */ - IF EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Backup%' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_Backup jobs are enabled, so gracefully exiting. You do not want to accidentally do restores over top of the databases you are backing up.', 0, 1) WITH NOWAIT; - RETURN; - END - - IF OBJECT_ID('msdb.dbo.restore_worker') IS NOT NULL - - BEGIN - - /* - - Make sure configuration table exists... - - */ - - IF OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL - - BEGIN - - IF @Debug = 1 RAISERROR('Checking variables', 0, 1) WITH NOWAIT; - - /* - - These settings are configurable - - */ - - SELECT @rto = CONVERT(INT, configuration_setting) - FROM msdb.dbo.restore_configuration c - WHERE configuration_name = N'log restore frequency'; - - - IF @rto IS NULL - BEGIN - RAISERROR('@rto cannot be NULL. Please check the msdb.dbo.restore_configuration table', 0, 1) WITH NOWAIT; - RETURN; - END; - - - END; - - ELSE - - BEGIN - - RAISERROR('msdb.dbo.restore_configuration does not exist, please run setup script', 0, 1) WITH NOWAIT; - - RETURN; - - END; - - - WHILE @Restore = 1 - - /* - - Start loop to restore log backups - - */ - - - BEGIN - - BEGIN TRY - - BEGIN TRAN; - - IF @Debug = 1 RAISERROR('Begin tran to grab a database to restore', 0, 1) WITH NOWAIT; - - - /* - - This grabs a database for a worker to work on - - The locking hints hope to provide some isolation when 10+ workers are in action - - */ - - - SELECT TOP (1) - @database = rw.database_name, - @only_logs_after = REPLACE(REPLACE(REPLACE(CONVERT(NVARCHAR(30), rw.last_log_restore_start_time, 120), ' ', ''), '-', ''), ':', ''), - @restore_full = CASE WHEN rw.is_started = 0 - AND rw.is_completed = 0 - AND rw.last_log_restore_start_time = '1900-01-01 00:00:00.000' - AND rw.last_log_restore_finish_time = '9999-12-31 00:00:00.000' - THEN 1 - ELSE 0 - END - FROM msdb.dbo.restore_worker rw WITH (UPDLOCK, HOLDLOCK, ROWLOCK) - WHERE - ( /*This section works on databases already part of the backup cycle*/ - rw.is_started = 0 - AND rw.is_completed = 1 - AND rw.last_log_restore_start_time < DATEADD(SECOND, (@rto * -1), GETDATE()) - AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ - ) - OR - ( /*This section picks up newly added databases by DiskPollster*/ - rw.is_started = 0 - AND rw.is_completed = 0 - AND rw.last_log_restore_start_time = '1900-01-01 00:00:00.000' - AND rw.last_log_restore_finish_time = '9999-12-31 00:00:00.000' - AND (rw.error_number IS NULL OR rw.error_number > 0) /* negative numbers indicate human attention required */ - ) - ORDER BY rw.last_log_restore_start_time ASC, rw.last_log_restore_finish_time ASC, rw.database_name ASC; - - - IF @database IS NOT NULL - BEGIN - SET @msg = N'Updating restore_worker for database ' + ISNULL(@database, 'UH OH NULL @database'); - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - /* - - Update the worker table so other workers know a database is being restored + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 11 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + BEGIN - */ - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 11) WITH NOWAIT; - UPDATE rw - SET rw.is_started = 1, - rw.is_completed = 0, - rw.last_log_restore_start_time = GETDATE() - FROM msdb.dbo.restore_worker rw - WHERE rw.database_name = @database; - END - - COMMIT; - - END TRY - - BEGIN CATCH - - /* - - Do I need to build retry logic in here? Try to catch deadlocks? I don't know yet! - - */ + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 11 AS CheckID, + 100 AS Priority, + ''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);'; - SELECT @msg = N'Error securing a database to restore, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + 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.'; - SET @database = NULL; - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - END CATCH; + EXECUTE(@StringToExecute); + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 12 ) + BEGIN - /* If we don't find a database to work on, wait for a few seconds */ - IF @database IS NULL + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 12) WITH NOWAIT; - BEGIN - IF @Debug = 1 RAISERROR('No databases to restore up right now, starting 3 second throttle', 0, 1) WITH NOWAIT; - WAITFOR DELAY '00:00:03.000'; - - /* Check to make sure backup jobs aren't enabled */ - IF EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Backup%' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_Backup jobs are enabled, so gracefully exiting. You do not want to accidentally do restores over top of the databases you are backing up.', 0, 1) WITH NOWAIT; - RETURN; - END - - /* Check to make sure job is still enabled */ - IF NOT EXISTS ( - SELECT * - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Restore%' - AND enabled = 1 - ) - BEGIN - RAISERROR('sp_AllNightLog_Restore jobs are disabled, so gracefully exiting. It feels graceful to me, anyway.', 0, 1) WITH NOWAIT; - RETURN; - END + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 12 AS CheckID , + [name] AS DatabaseName , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Close Enabled' AS Finding , + 'https://www.brentozar.com/go/autoclose' AS URL , + ( 'Database [' + [name] + + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details + FROM sys.databases + WHERE is_auto_close_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 12); + END; - END - - - BEGIN TRY + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 13 ) + BEGIN - BEGIN - - IF @database IS NOT NULL - - /* - - Make sure we have a database to work on -- I should make this more robust so we do something if it is NULL, maybe - - */ - - - BEGIN - - SET @msg = CASE WHEN @restore_full = 0 - THEN N'Restoring logs for ' - ELSE N'Restoring full backup for ' - END - + ISNULL(@database, 'UH OH NULL @database'); - - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - /* - - Call sp_DatabaseRestore to backup the database - - */ - - SET @restore_path_full = @restore_path_base + N'\' + @database + N'\' + N'FULL\' - - SET @msg = N'Path for FULL backups for ' + @database + N' is ' + @restore_path_full - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - SET @restore_path_log = @restore_path_base + N'\' + @database + N'\' + N'LOG\' - - SET @msg = N'Path for LOG backups for ' + @database + N' is ' + @restore_path_log - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - IF @restore_full = 0 - - BEGIN - - IF @Debug = 1 RAISERROR('Starting Log only restores', 0, 1) WITH NOWAIT; - - EXEC master.dbo.sp_DatabaseRestore @Database = @database, - @BackupPathFull = @restore_path_full, - @BackupPathLog = @restore_path_log, - @ContinueLogs = 1, - @RunRecovery = 0, - @OnlyLogsAfter = @only_logs_after, - @Debug = @Debug - - END - - IF @restore_full = 1 - - BEGIN - - IF @Debug = 1 RAISERROR('Starting first Full restore from: ', 0, 1) WITH NOWAIT; - IF @Debug = 1 RAISERROR(@restore_path_full, 0, 1) WITH NOWAIT; - - EXEC master.dbo.sp_DatabaseRestore @Database = @database, - @BackupPathFull = @restore_path_full, - @BackupPathLog = @restore_path_log, - @ContinueLogs = 0, - @RunRecovery = 0, - @Debug = @Debug - - END - - - - /* - - Catch any erroneous zones - - */ - - SELECT @error_number = ERROR_NUMBER(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - END; --End call to dbo.sp_DatabaseRestore - - END; --End successful check of @database (not NULL) - - END TRY - - BEGIN CATCH - - IF @error_number IS NOT NULL - - /* + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 13) WITH NOWAIT; - If the ERROR() function returns a number, update the table with it and the last error date. - - Also update the last start time to 1900-01-01 so it gets picked back up immediately -- the query to find a log restore to take sorts by start time + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 13 AS CheckID , + [name] AS DatabaseName , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Shrink Enabled' AS Finding , + 'https://www.brentozar.com/go/autoshrink' AS URL , + ( 'Database [' + [name] + + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details + FROM sys.databases + WHERE is_auto_shrink_on = 1 + AND state <> 6 /* Offline */ + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 13); + END; - */ - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 14 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' BEGIN - - SET @msg = N'Error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()); - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; - SET @msg = N'Updating restore_worker for database ' + ISNULL(@database, 'UH OH NULL @database') + ' for unsuccessful backup'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - - - UPDATE rw - SET rw.is_started = 0, - rw.is_completed = 1, - rw.last_log_restore_start_time = '19000101', - rw.error_number = @error_number, - rw.last_error_date = GETDATE() - FROM msdb.dbo.restore_worker rw - WHERE rw.database_name = @database; - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 14) WITH NOWAIT; - /* + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 14 AS CheckID, + [name] as DatabaseName, + 50 AS Priority, + ''Reliability'' AS FindingsGroup, + ''Page Verification Not Optimal'' AS Finding, + ''https://www.brentozar.com/go/torn'' AS URL, + (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details + FROM sys.databases + WHERE page_verify_option < 2 + AND name <> ''tempdb'' + AND state NOT IN (1, 6) /* Restoring, Offline */ + and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 14) OPTION (RECOMPILE);'; - Set @database back to NULL to avoid variable assignment weirdness + 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.'; - */ + EXECUTE(@StringToExecute); + END; + END; - SET @database = NULL; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 15 ) + BEGIN - - /* - - Wait around for a second so we're not just spinning wheels -- this only runs if the BEGIN CATCH is triggered by an error - - */ - - IF @Debug = 1 RAISERROR('Starting 1 second throttle', 0, 1) WITH NOWAIT; - - WAITFOR DELAY '00:00:01.000'; - - END; -- End update of unsuccessful restore - - END CATCH; - - - IF @database IS NOT NULL AND @error_number IS NULL + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 15) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 15 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Create Stats Disabled' AS Finding , + 'https://www.brentozar.com/go/acs' AS URL , + ( 'Database [' + [name] + + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details + FROM sys.databases + WHERE is_auto_create_stats_on = 0 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 15); + END; - /* + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 16 ) + BEGIN - If no error, update everything normally + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 16) WITH NOWAIT; - */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 16 AS CheckID , + [name] AS DatabaseName , + 110 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Update Stats Disabled' AS Finding , + 'https://www.brentozar.com/go/aus' AS URL , + ( 'Database [' + [name] + + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details + FROM sys.databases + WHERE is_auto_update_stats_on = 0 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 16); + END; - - BEGIN - - IF @Debug = 1 RAISERROR('Error number IS NULL', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 17 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 17) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 17 AS CheckID , + [name] AS DatabaseName , + 150 AS Priority , + 'Performance' AS FindingsGroup , + 'Stats Updated Asynchronously' AS Finding , + 'https://www.brentozar.com/go/asyncstats' AS URL , + ( 'Database [' + [name] + + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details + FROM sys.databases + WHERE is_auto_update_stats_async_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 17); + END; - /* Make sure database actually exists and is in the restoring state */ - IF EXISTS (SELECT * FROM sys.databases WHERE name = @database AND state = 1) /* Restoring */ - BEGIN - SET @msg = N'Updating backup_worker for database ' + ISNULL(@database, 'UH OH NULL @database') + ' for successful backup'; - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; - - UPDATE rw - SET rw.is_started = 0, - rw.is_completed = 1, - rw.last_log_restore_finish_time = GETDATE() - FROM msdb.dbo.restore_worker rw - WHERE rw.database_name = @database; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 20 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 20 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Date Correlation On' AS Finding , + 'https://www.brentozar.com/go/corr' AS URL , + ( 'Database [' + [name] + + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details + FROM sys.databases + WHERE is_date_correlation_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 20); + END; - END - ELSE /* The database doesn't exist, or it's not in the restoring state */ - BEGIN - SET @msg = N'Updating backup_worker for database ' + ISNULL(@database, 'UH OH NULL @database') + ' for UNsuccessful backup'; - IF @Debug = 1 RAISERROR(@msg, 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 21 ) + BEGIN + /* --TOURSTOP04-- */ + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN - UPDATE rw - SET rw.is_started = 0, - rw.is_completed = 1, - rw.error_number = -1, /* unknown, human attention required */ - rw.last_error_date = GETDATE() - /* rw.last_log_restore_finish_time = GETDATE() don't change this - the last log may still be successful */ - FROM msdb.dbo.restore_worker rw - WHERE rw.database_name = @database; - END - - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 21) WITH NOWAIT; - /* + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 21 AS CheckID, + [name] as DatabaseName, + 200 AS Priority, + ''Informational'' AS FindingsGroup, + ''Database Encrypted'' AS Finding, + ''https://www.brentozar.com/go/tde'' AS URL, + (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details + FROM sys.databases + WHERE is_encrypted = 1 + and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 21) OPTION (RECOMPILE);'; - Set @database back to NULL to avoid variable assignment weirdness + 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.'; - */ - - SET @database = NULL; - - - END; -- End update for successful backup - - END; -- End @Restore WHILE loop - - - END; -- End successful check for restore_worker and subsequent code - - - ELSE - - BEGIN - - RAISERROR('msdb.dbo.restore_worker does not exist, please run setup script', 0, 1) WITH NOWAIT; - - RETURN; - - END; -RETURN; - - - -END; -- Final END for stored proc - -GO -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO + EXECUTE(@StringToExecute); + END; + END; -IF OBJECT_ID('dbo.sp_AllNightLog_Setup') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_AllNightLog_Setup AS RETURN 0;'); -GO + /* + Believe it or not, SQL Server doesn't track the default values + for sp_configure options! We'll make our own list here. + */ + IF @Debug IN (1, 2) RAISERROR('Generating default configuration values', 0, 1) WITH NOWAIT; -ALTER PROCEDURE dbo.sp_AllNightLog_Setup - @RPOSeconds BIGINT = 30, - @RTOSeconds BIGINT = 30, - @BackupPath NVARCHAR(MAX) = NULL, - @RestorePath NVARCHAR(MAX) = NULL, - @Jobs TINYINT = 10, - @RunSetup BIT = 0, - @UpdateSetup BIT = 0, - @EnableBackupJobs INT = NULL, - @EnableRestoreJobs INT = NULL, - @Debug BIT = 0, - @FirstFullBackup BIT = 0, - @FirstDiffBackup BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -SET NOCOUNT ON; + 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 ('min server memory (MB)', 0, 1036); + END; -BEGIN; -DECLARE @Version VARCHAR(30); -SET @Version = '2.0'; -SET @VersionDate = '20171201'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 22 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 22) WITH NOWAIT; -IF @Help = 1 + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT cd.CheckID , + 200 AS Priority , + 'Non-Default Server Config' AS FindingsGroup , + cr.name AS Finding , + 'https://www.brentozar.com/go/conf' AS URL , + ( 'This sp_configure option has been changed. Its default value is ' + + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), + '(unknown)') + + ' and it has been set to ' + + CAST(cr.value_in_use AS VARCHAR(100)) + + '.' ) AS Details + FROM sys.configurations cr + INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name + LEFT OUTER JOIN #ConfigurationDefaults cdUsed ON cdUsed.name = cr.name + AND cdUsed.DefaultValue = cr.value_in_use + WHERE cdUsed.name IS NULL; + END; -BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 190 ) + BEGIN - PRINT ' - /* + IF @Debug IN (1, 2) RAISERROR('Setting @MinServerMemory and @MaxServerMemory', 0, 1) WITH NOWAIT; + SELECT @MinServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'min server memory (MB)'; + SELECT @MaxServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'max server memory (MB)'; + + IF (@MinServerMemory = @MaxServerMemory) + BEGIN - sp_AllNightLog_Setup from http://FirstResponderKit.org - - This script sets up a database, tables, rows, and jobs for sp_AllNightLog, including: + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 190) WITH NOWAIT; - * Creates a database - * Right now it''s hard-coded to use msdbCentral, that might change later - - * Creates tables in that database! - * dbo.backup_configuration - * Hold variables used by stored proc to make runtime decisions - * RPO: Seconds, how often we look for databases that need log backups - * Backup Path: The path we feed to Ola H''s backup proc - * dbo.backup_worker - * Holds list of databases and some information that helps our Agent jobs figure out if they need to take another log backup - - * Creates tables in msdb - * dbo.restore_configuration - * Holds variables used by stored proc to make runtime decisions - * RTO: Seconds, how often to look for log backups to restore - * Restore Path: The path we feed to sp_DatabaseRestore - * dbo.restore_worker - * Holds list of databases and some information that helps our Agent jobs figure out if they need to look for files to restore - - * Creates agent jobs - * 1 job that polls sys.databases for new entries - * 10 jobs that run to take log backups - * Based on a queue table - * Requires Ola Hallengren''s Database Backup stored proc - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000! And really, maybe not even anything less than 2016. Heh. - - The repository database name is hard-coded to msdbCentral. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @RunSetup BIT, defaults to 0. When this is set to 1, it will run the setup portion to create database, tables, and worker jobs. - @UpdateSetup BIT, defaults to 0. When set to 1, will update existing configs for RPO/RTO and database backup/restore paths. - @RPOSeconds BIGINT, defaults to 30. Value in seconds you want to use to determine if a new log backup needs to be taken. - @BackupPath NVARCHAR(MAX), defaults to = ''D:\Backup''. You 99.99999% will need to change this path to something else. This tells Ola''s job where to put backups. - @Debug BIT, defaults to 0. Whent this is set to 1, it prints out dynamic SQL commands - - Sample call: - EXEC dbo.sp_AllNightLog_Setup - @RunSetup = 1, - @RPOSeconds = 30, - @BackupPath = N''M:\MSSQL\Backup'', - @Debug = 1 - - - For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - - */'; - -RETURN; -END; /* IF @Help = 1 */ - -DECLARE @database NVARCHAR(128) = NULL; --Holds the database that's currently being processed -DECLARE @error_number INT = NULL; --Used for TRY/CATCH -DECLARE @error_severity INT; --Used for TRY/CATCH -DECLARE @error_state INT; --Used for TRY/CATCH -DECLARE @msg NVARCHAR(4000) = N''; --Used for RAISERROR -DECLARE @rpo INT; --Used to hold the RPO value in our configuration table -DECLARE @backup_path NVARCHAR(MAX); --Used to hold the backup path in our configuration table -DECLARE @db_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL to create msdbCentral -DECLARE @tbl_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL that creates tables in msdbCentral -DECLARE @database_name NVARCHAR(256) = N'msdbCentral'; --Used to hold the name of the database we create to centralize data - --Right now it's hardcoded to msdbCentral, but I made it dynamic in case that changes down the line - - -/*These variables control the loop to create/modify jobs*/ -DECLARE @job_sql NVARCHAR(MAX) = N''; --Used to hold the dynamic SQL that creates Agent jobs -DECLARE @counter INT = 0; --For looping to create 10 Agent jobs -DECLARE @job_category NVARCHAR(MAX) = N'''Database Maintenance'''; --Job category -DECLARE @job_owner NVARCHAR(128) = QUOTENAME(SUSER_SNAME(0x01), ''''); -- Admin user/owner -DECLARE @jobs_to_change TABLE(name SYSNAME); -- list of jobs we need to enable or disable -DECLARE @current_job_name SYSNAME; -- While looping through Agent jobs to enable or disable -DECLARE @active_start_date INT = (CONVERT(INT, CONVERT(VARCHAR(10), GETDATE(), 112))); -DECLARE @started_waiting_for_jobs DATETIME; --We need to wait for a while when disabling jobs - -/*Specifically for Backups*/ -DECLARE @job_name_backups NVARCHAR(MAX) = N'''sp_AllNightLog_Backup_Job_'''; --Name of log backup job -DECLARE @job_description_backups NVARCHAR(MAX) = N'''This is a worker for the purposes of taking log backups from msdbCentral.dbo.backup_worker queue table.'''; --Job description -DECLARE @job_command_backups NVARCHAR(MAX) = N'''EXEC sp_AllNightLog @Backup = 1'''; --Command the Agent job will run - -/*Specifically for Restores*/ -DECLARE @job_name_restores NVARCHAR(MAX) = N'''sp_AllNightLog_Restore_Job_'''; --Name of log backup job -DECLARE @job_description_restores NVARCHAR(MAX) = N'''This is a worker for the purposes of restoring log backups from msdb.dbo.restore_worker queue table.'''; --Job description -DECLARE @job_command_restores NVARCHAR(MAX) = N'''EXEC sp_AllNightLog @Restore = 1'''; --Command the Agent job will run - - -/* - -Sanity check some variables - -*/ - - - -IF ((@RunSetup = 0 OR @RunSetup IS NULL) AND (@UpdateSetup = 0 OR @UpdateSetup IS NULL)) - - BEGIN - - RAISERROR('You have to either run setup or update setup. You can''t not do neither nor, if you follow. Or not.', 0, 1) WITH NOWAIT; - - RETURN; - - END; - - -/* - -Should be a positive number - -*/ - -IF (@RPOSeconds < 0) - - BEGIN - RAISERROR('Please choose a positive number for @RPOSeconds', 0, 1) WITH NOWAIT; - - RETURN; - END; - - -/* - -Probably shouldn't be more than 20 - -*/ - -IF (@Jobs > 20) OR (@Jobs < 1) - - BEGIN - RAISERROR('We advise sticking with 1-20 jobs.', 0, 1) WITH NOWAIT; - - RETURN; - END; - -/* - -Probably shouldn't be more than 4 hours - -*/ - -IF (@RPOSeconds >= 14400) - BEGIN - - RAISERROR('If your RPO is really 4 hours, perhaps you''d be interested in a more modest recovery model, like SIMPLE?', 0, 1) WITH NOWAIT; - - RETURN; - END; - - -/* - -Can't enable both the backup and restore jobs at the same time - -*/ - -IF @EnableBackupJobs = 1 AND @EnableRestoreJobs = 1 - BEGIN - - RAISERROR('You are not allowed to enable both the backup and restore jobs at the same time. Pick one, bucko.', 0, 1) WITH NOWAIT; - - RETURN; - END; - -/* -Make sure xp_cmdshell is enabled -*/ -IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND value_in_use = 1) - BEGIN - RAISERROR('xp_cmdshell must be enabled so we can get directory contents to check for new databases to restore.', 0, 1) WITH NOWAIT - - RETURN; - END - -/* -Make sure Ola Hallengren's scripts are installed in master -*/ -IF 2 <> (SELECT COUNT(*) FROM master.sys.procedures WHERE name IN('CommandExecute', 'DatabaseBackup')) - BEGIN - RAISERROR('Ola Hallengren''s CommandExecute and DatabaseBackup must be installed in the master database. More info: http://ola.hallengren.com', 0, 1) WITH NOWAIT - - RETURN; - END - -/* -Make sure sp_DatabaseRestore is installed in master -*/ -IF NOT EXISTS (SELECT * FROM master.sys.procedures WHERE name = 'sp_DatabaseRestore') - BEGIN - RAISERROR('sp_DatabaseRestore must be installed in master. To get it: http://FirstResponderKit.org', 0, 1) WITH NOWAIT - - RETURN; - END - -/* - -Basic path sanity checks - -*/ - -IF (@BackupPath NOT LIKE '[c-zC-Z]:\%') --Local path, don't think anyone has A or B drives -AND (@BackupPath NOT LIKE '\\[a-zA-Z0-9]%\%') --UNC path - - BEGIN - RAISERROR('Are you sure that''s a real path?', 0, 1) WITH NOWAIT; - - RETURN; - END; - -/* - -If you want to update the table, one of these has to not be NULL - -*/ - -IF @UpdateSetup = 1 - AND ( @RPOSeconds IS NULL - AND @BackupPath IS NULL - AND @RPOSeconds IS NULL - AND @RestorePath IS NULL - AND @EnableBackupJobs IS NULL - AND @EnableRestoreJobs IS NULL - ) - - BEGIN - - RAISERROR('If you want to update configuration settings, they can''t be NULL. Please Make sure @RPOSeconds / @RTOSeconds or @BackupPath / @RestorePath has a value', 0, 1) WITH NOWAIT; - - RETURN; - - END; - - -IF @UpdateSetup = 1 - GOTO UpdateConfigs; - -IF @RunSetup = 1 -BEGIN - BEGIN TRY - - BEGIN - - - /* - - First check to see if Agent is running -- we'll get errors if it's not - - */ - - - IF ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) IS NOT NULL - - BEGIN - - IF EXISTS ( - SELECT 1 - FROM sys.dm_server_services - WHERE servicename LIKE 'SQL Server Agent%' - AND status_desc = 'Stopped' - ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES + ( 190, + 200, + 'Performance', + 'Non-Dynamic Memory', + 'https://www.brentozar.com/go/memory', + 'Minimum Server Memory setting is the same as the Maximum (both set to ' + CAST(@MinServerMemory AS NVARCHAR(50)) + '). This will not allow dynamic memory. Please revise memory settings' + ); + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 188 ) BEGIN - - RAISERROR('SQL Server Agent is not currently running -- it needs to be enabled to add backup worker jobs and the new database polling job', 0, 1) WITH NOWAIT; - - RETURN; - - END; - - END - - BEGIN - - - /* + /* Let's set variables so that our query is still SARGable */ - Check to see if the database exists - - */ - - RAISERROR('Checking for msdbCentral', 0, 1) WITH NOWAIT; - - SET @db_sql += N' - - IF DATABASEPROPERTYEX(' + QUOTENAME(@database_name, '''') + ', ''Status'') IS NULL - - BEGIN - - RAISERROR(''Creating msdbCentral'', 0, 1) WITH NOWAIT; - - CREATE DATABASE ' + QUOTENAME(@database_name) + '; - - ALTER DATABASE ' + QUOTENAME(@database_name) + ' SET RECOVERY FULL; - - END - - '; - - - IF @Debug = 1 - BEGIN - RAISERROR(@db_sql, 0, 1) WITH NOWAIT; - END; - - - IF @db_sql IS NULL - BEGIN - RAISERROR('@db_sql is NULL for some reason', 0, 1) WITH NOWAIT; - END; - - - EXEC sp_executesql @db_sql; - - - /* + IF @Debug IN (1, 2) RAISERROR('Setting @Processors.', 0, 1) WITH NOWAIT; - Check for tables and stuff - - */ - + SET @Processors = (SELECT cpu_count FROM sys.dm_os_sys_info); + + IF @Debug IN (1, 2) RAISERROR('Setting @NUMANodes', 0, 1) WITH NOWAIT; + + SET @NUMANodes = (SELECT COUNT(1) + FROM sys.dm_os_performance_counters pc + WHERE pc.object_name LIKE '%Buffer Node%' + AND counter_name = 'Page life expectancy'); + /* If Cost Threshold for Parallelism is default then flag as a potential issue */ + /* If MAXDOP is default and processors > 8 or NUMA nodes > 1 then flag as potential issue */ + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 188) WITH NOWAIT; - RAISERROR('Checking for tables in msdbCentral', 0, 1) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 188 AS CheckID , + 200 AS Priority , + 'Performance' AS FindingsGroup , + cr.name AS Finding , + 'https://www.brentozar.com/go/cxpacket' AS URL , + ( 'Set to ' + CAST(cr.value_in_use AS NVARCHAR(50)) + ', its default value. Changing this sp_configure setting may reduce CXPACKET waits.') + FROM sys.configurations cr + INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name + AND cr.value_in_use = cd.DefaultValue + WHERE cr.name = 'cost threshold for parallelism' + OR (cr.name = 'max degree of parallelism' AND (@NUMANodes > 1 OR @Processors > 8)); + END; - SET @tbl_sql += N' - - USE ' + QUOTENAME(@database_name) + ' - - - IF OBJECT_ID(''' + QUOTENAME(@database_name) + '.dbo.backup_configuration'') IS NULL - - BEGIN - - RAISERROR(''Creating table dbo.backup_configuration'', 0, 1) WITH NOWAIT; - - CREATE TABLE dbo.backup_configuration ( - database_name NVARCHAR(256), - configuration_name NVARCHAR(512), - configuration_description NVARCHAR(512), - configuration_setting NVARCHAR(MAX) - ); - - END - - ELSE - - BEGIN - - - RAISERROR(''Backup configuration table exists, truncating'', 0, 1) WITH NOWAIT; - - - TRUNCATE TABLE dbo.backup_configuration - - - END - - - RAISERROR(''Inserting configuration values'', 0, 1) WITH NOWAIT; - - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''log backup frequency'', ''The length of time in second between Log Backups.'', ''' + CONVERT(NVARCHAR(10), @RPOSeconds) + '''); - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''log backup path'', ''The path to which Log Backups should go.'', ''' + @BackupPath + '''); - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''change backup type'', ''For Ola Hallengren DatabaseBackup @ChangeBackupType param: Y = escalate to fulls, MSDB = escalate by checking msdb backup history.'', ''MSDB''); - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''encrypt'', ''For Ola Hallengren DatabaseBackup: Y = encrypt the backup. N (default) = do not encrypt.'', NULL); - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''encryptionalgorithm'', ''For Ola Hallengren DatabaseBackup: native 2014 choices include TRIPLE_DES_3KEY, AES_128, AES_192, AES_256.'', NULL); - - INSERT dbo.backup_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES (''all'', ''servercertificate'', ''For Ola Hallengren DatabaseBackup: server certificate that is used to encrypt the backup.'', NULL); - - - IF OBJECT_ID(''' + QUOTENAME(@database_name) + '.dbo.backup_worker'') IS NULL - - BEGIN - - - RAISERROR(''Creating table dbo.backup_worker'', 0, 1) WITH NOWAIT; - - CREATE TABLE dbo.backup_worker ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - database_name NVARCHAR(256), - last_log_backup_start_time DATETIME DEFAULT ''19000101'', - last_log_backup_finish_time DATETIME DEFAULT ''99991231'', - is_started BIT DEFAULT 0, - is_completed BIT DEFAULT 0, - error_number INT DEFAULT NULL, - last_error_date DATETIME DEFAULT NULL, - ignore_database BIT DEFAULT 0, - full_backup_required BIT DEFAULT ' + CASE WHEN @FirstFullBackup = 0 THEN N'0,' ELSE N'1,' END + CHAR(10) + - N'diff_backup_required BIT DEFAULT ' + CASE WHEN @FirstDiffBackup = 0 THEN N'0' ELSE N'1' END + CHAR(10) + - N'); - - END; - - ELSE - - BEGIN - - - RAISERROR(''Backup worker table exists, truncating'', 0, 1) WITH NOWAIT; - - - TRUNCATE TABLE dbo.backup_worker - - - END - - - RAISERROR(''Inserting databases for backups'', 0, 1) WITH NOWAIT; - - INSERT ' + QUOTENAME(@database_name) + '.dbo.backup_worker (database_name) - SELECT d.name - FROM sys.databases d - WHERE NOT EXISTS ( - SELECT * - FROM msdbCentral.dbo.backup_worker bw - WHERE bw.database_name = d.name - ) - AND d.database_id > 4; - - '; - - - IF @Debug = 1 - BEGIN - SET @msg = SUBSTRING(@tbl_sql, 0, 2044) - RAISERROR(@msg, 0, 1) WITH NOWAIT; - SET @msg = SUBSTRING(@tbl_sql, 2044, 4088) - RAISERROR(@msg, 0, 1) WITH NOWAIT; - SET @msg = SUBSTRING(@tbl_sql, 4088, 6132) - RAISERROR(@msg, 0, 1) WITH NOWAIT; - SET @msg = SUBSTRING(@tbl_sql, 6132, 8176) - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - - IF @tbl_sql IS NULL - BEGIN - RAISERROR('@tbl_sql is NULL for some reason', 0, 1) WITH NOWAIT; - END; - - - EXEC sp_executesql @tbl_sql; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 24 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 24) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 24 AS CheckID , + DB_NAME(database_id) AS DatabaseName , + 170 AS Priority , + 'File Configuration' AS FindingsGroup , + 'System Database on C Drive' AS Finding , + 'https://www.brentozar.com/go/cdrive' AS URL , + ( 'The ' + DB_NAME(database_id) + + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) IN ( 'master', + 'model', 'msdb' ); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 25 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN - /* + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 25) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 + 25 AS CheckID , + 'tempdb' , + 20 AS Priority , + 'File Configuration' AS FindingsGroup , + 'TempDB on C Drive' AS Finding , + 'https://www.brentozar.com/go/cdrive' AS URL , + CASE WHEN growth > 0 + THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) + ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) + END AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) = 'tempdb'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 26 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN - This section creates tables for restore workers to work off of + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 26) WITH NOWAIT; - */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 26 AS CheckID , + DB_NAME(database_id) AS DatabaseName , + 20 AS Priority , + 'Reliability' AS FindingsGroup , + 'User Databases on C Drive' AS Finding , + 'https://www.brentozar.com/go/cdrive' AS URL , + ( 'The ' + DB_NAME(database_id) + + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details + FROM sys.master_files + WHERE UPPER(LEFT(physical_name, 1)) = 'C' + AND DB_NAME(database_id) NOT IN ( 'master', + 'model', 'msdb', + 'tempdb' ) + AND DB_NAME(database_id) NOT IN ( + SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 26 ); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 27 ) + BEGIN - /* + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 27) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 27 AS CheckID , + 'master' AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the Master Database' AS Finding , + 'https://www.brentozar.com/go/mastuser' AS URL , + ( 'The ' + name + + ' table in the master database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. 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'); + /* That last one is the Dynamics NAV licensing table: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2426 */ + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 28 ) + BEGIN - In search of msdb + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 28) WITH NOWAIT; - */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 28 AS CheckID , + 'msdb' AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the MSDB Database' AS Finding , + 'https://www.brentozar.com/go/msdbuser' AS URL , + ( 'The ' + name + + ' table in the msdb database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. Tables in the msdb database may not be restored in the event of a disaster.' ) AS Details + FROM msdb.sys.tables + WHERE is_ms_shipped = 0 AND name NOT LIKE '%DTA_%'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 29 ) + BEGIN - RAISERROR('Checking for msdb. Yeah, I know...', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 29) WITH NOWAIT; - IF DATABASEPROPERTYEX('msdb', 'Status') IS NULL + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 29 AS CheckID , + 'model' AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Tables in the Model Database' AS Finding , + 'https://www.brentozar.com/go/model' AS URL , + ( 'The ' + name + + ' table in the model database was created by end users on ' + + CAST(create_date AS VARCHAR(20)) + + '. Tables in the model database are automatically copied into all new databases.' ) AS Details + FROM model.sys.tables + WHERE is_ms_shipped = 0; + END; - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 30 ) + BEGIN + IF ( SELECT COUNT(*) + FROM msdb.dbo.sysalerts + WHERE severity BETWEEN 19 AND 25 + ) < 7 - RAISERROR('YOU HAVE NO MSDB WHY?!', 0, 1) WITH NOWAIT; + BEGIN - RETURN; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 30) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 30 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'Not All Alerts Configured' AS Finding , + 'https://www.brentozar.com/go/alert' AS URL , + ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; END; + END; - - /* In search of restore_configuration */ - - RAISERROR('Checking for Restore Worker tables in msdb', 0, 1) WITH NOWAIT; - - IF OBJECT_ID('msdb.dbo.restore_configuration') IS NULL - - BEGIN - - RAISERROR('Creating restore_configuration table in msdb', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 59 ) + BEGIN + IF EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE enabled = 1 + AND COALESCE(has_notification, 0) = 0 + AND (job_id IS NULL OR job_id = 0x)) - CREATE TABLE msdb.dbo.restore_configuration ( - database_name NVARCHAR(256), - configuration_name NVARCHAR(512), - configuration_description NVARCHAR(512), - configuration_setting NVARCHAR(MAX) - ); + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 59) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 59 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'Alerts Configured without Follow Up' AS Finding , + 'https://www.brentozar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 96 ) + BEGIN + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE message_id IN ( 823, 824, 825 ) ) + + BEGIN; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 96) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 96 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'No Alerts for Corruption' AS Finding , + 'https://www.brentozar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; + + END; + END; - ELSE - - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 61 ) + BEGIN + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysalerts + WHERE severity BETWEEN 19 AND 25 ) + BEGIN - - RAISERROR('Restore configuration table exists, truncating', 0, 1) WITH NOWAIT; - - TRUNCATE TABLE msdb.dbo.restore_configuration; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 61) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 61 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'No Alerts for Sev 19-25' AS Finding , + 'https://www.brentozar.com/go/alert' AS URL , + ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; + END; + END; - RAISERROR('Inserting configuration values to msdb.dbo.restore_configuration', 0, 1) WITH NOWAIT; - - INSERT msdb.dbo.restore_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES ('all', 'log restore frequency', 'The length of time in second between Log Restores.', @RTOSeconds); - - INSERT msdb.dbo.restore_configuration (database_name, configuration_name, configuration_description, configuration_setting) - VALUES ('all', 'log restore path', 'The path to which Log Restores come from.', @RestorePath); - - - IF OBJECT_ID('msdb.dbo.restore_worker') IS NULL + --check for disabled alerts + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 98 ) + BEGIN + IF EXISTS ( SELECT name + FROM msdb.dbo.sysalerts + WHERE enabled = 0 ) BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 98) WITH NOWAIT; - RAISERROR('Creating table msdb.dbo.restore_worker', 0, 1) WITH NOWAIT; - - CREATE TABLE msdb.dbo.restore_worker ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - database_name NVARCHAR(256), - last_log_restore_start_time DATETIME DEFAULT '19000101', - last_log_restore_finish_time DATETIME DEFAULT '99991231', - is_started BIT DEFAULT 0, - is_completed BIT DEFAULT 0, - error_number INT DEFAULT NULL, - last_error_date DATETIME DEFAULT NULL, - ignore_database BIT DEFAULT 0, - full_backup_required BIT DEFAULT 0, - diff_backup_required BIT DEFAULT 0 - ); + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 98 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'Alerts Disabled' AS Finding , + 'https://www.brentozar.com/go/alert' AS URL , + ( 'The following Alert is disabled, please review and enable if desired: ' + + name ) AS Details + FROM msdb.dbo.sysalerts + WHERE enabled = 0; + END; + END; - - RAISERROR('Inserting databases for restores', 0, 1) WITH NOWAIT; - - INSERT msdb.dbo.restore_worker (database_name) - SELECT d.name - FROM sys.databases d - WHERE NOT EXISTS ( - SELECT * - FROM msdb.dbo.restore_worker bw - WHERE bw.database_name = d.name - ) - AND d.database_id > 4; + --check for alerts that do NOT include event descriptions in their outputs via email/pager/net-send + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 219 + ) + BEGIN; + IF @Debug IN (1, 2) + BEGIN; + RAISERROR ('Running CheckId [%d].', 0, 1, 219) WITH NOWAIT; + END; + + INSERT INTO #BlitzResults ( + CheckID + ,[Priority] + ,FindingsGroup + ,Finding + ,[URL] + ,Details + ) + SELECT 219 AS CheckID + ,200 AS [Priority] + ,'Monitoring' AS FindingsGroup + ,'Alerts Without Event Descriptions' AS Finding + ,'https://www.brentozar.com/go/alert' AS [URL] + ,('The following Alert is not including detailed event descriptions in its output messages: ' + QUOTENAME([name]) + + '. You can fix it by ticking the relevant boxes in its Properties --> Options page.') AS Details + FROM msdb.dbo.sysalerts + WHERE [enabled] = 1 + AND include_event_description = 0 --bitmask: 1 = email, 2 = pager, 4 = net send + ; + END; + + --check whether we have NO ENABLED operators! + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 31 ) + BEGIN; + IF NOT EXISTS ( SELECT * + FROM msdb.dbo.sysoperators + WHERE enabled = 1 ) + + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 31) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 31 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'No Operators Configured/Enabled' AS Finding , + 'https://www.brentozar.com/go/op' AS URL , + ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 34 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_db_mirroring_auto_page_repair' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 34) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 34 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''https://www.brentozar.com/go/repair'' AS URL , + ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details + FROM (SELECT rp2.database_id, rp2.modification_time + FROM sys.dm_db_mirroring_auto_page_repair rp2 + WHERE rp2.[database_id] not in ( + SELECT db2.[database_id] + FROM sys.databases as db2 + WHERE db2.[state] = 1 + ) ) as rp + INNER JOIN master.sys.databases db ON rp.database_id = db.database_id + WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; - - /* - - Add Jobs - - */ - - - - /* - - Look for our ten second schedule -- all jobs use this to restart themselves if they fail + 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.'; - Fun fact: you can add the same schedule name multiple times, so we don't want to just stick it in there - - */ + EXECUTE(@StringToExecute); + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 89 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_hadr_auto_page_repair' ) + BEGIN - RAISERROR('Checking for ten second schedule', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 89) WITH NOWAIT; - IF NOT EXISTS ( - SELECT 1 - FROM msdb.dbo.sysschedules - WHERE name = 'ten_seconds' - ) - - BEGIN - - - RAISERROR('Creating ten second schedule', 0, 1) WITH NOWAIT; + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 89 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''https://www.brentozar.com/go/repair'' AS URL , + ( ''Availability Groups has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details + FROM sys.dm_hadr_auto_page_repair rp + INNER JOIN master.sys.databases db ON rp.database_id = db.database_id + WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; + + EXECUTE(@StringToExecute); + END; + END; - - EXEC msdb.dbo.sp_add_schedule @schedule_name= ten_seconds, - @enabled = 1, - @freq_type = 4, - @freq_interval = 1, - @freq_subday_type = 2, - @freq_subday_interval = 10, - @freq_relative_interval = 0, - @freq_recurrence_factor = 0, - @active_start_date = @active_start_date, - @active_end_date = 99991231, - @active_start_time = 0, - @active_end_time = 235959; - - END; - - - /* - - Look for Backup Pollster job -- this job sets up our watcher for new databases to back up - - */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 90 ) + BEGIN + IF EXISTS ( SELECT * + FROM msdb.sys.all_objects + WHERE name = 'suspect_pages' ) + BEGIN - - RAISERROR('Checking for pollster job', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 90) WITH NOWAIT; - - IF NOT EXISTS ( - SELECT 1 - FROM msdb.dbo.sysjobs - WHERE name = 'sp_AllNightLog_PollForNewDatabases' - ) - - - BEGIN - - - RAISERROR('Creating pollster job', 0, 1) WITH NOWAIT; - - IF @EnableBackupJobs = 1 - BEGIN - EXEC msdb.dbo.sp_add_job @job_name = sp_AllNightLog_PollForNewDatabases, - @description = 'This is a worker for the purposes of polling sys.databases for new entries to insert to the worker queue table.', - @category_name = 'Database Maintenance', - @owner_login_name = 'sa', - @enabled = 1; - END - ELSE - BEGIN - EXEC msdb.dbo.sp_add_job @job_name = sp_AllNightLog_PollForNewDatabases, - @description = 'This is a worker for the purposes of polling sys.databases for new entries to insert to the worker queue table.', - @category_name = 'Database Maintenance', - @owner_login_name = 'sa', - @enabled = 0; - END - - - RAISERROR('Adding job step', 0, 1) WITH NOWAIT; + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT + 90 AS CheckID , + db.name , + 1 AS Priority , + ''Corruption'' AS FindingsGroup , + ''Database Corruption Detected'' AS Finding , + ''https://www.brentozar.com/go/repair'' AS URL , + ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details + FROM msdb.dbo.suspect_pages sp + INNER JOIN master.sys.databases db ON sp.database_id = db.database_id + WHERE sp.last_update_date >= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; - - EXEC msdb.dbo.sp_add_jobstep @job_name = sp_AllNightLog_PollForNewDatabases, - @step_name = sp_AllNightLog_PollForNewDatabases, - @subsystem = 'TSQL', - @command = 'EXEC sp_AllNightLog @PollForNewDatabases = 1'; - - - - RAISERROR('Adding job server', 0, 1) WITH NOWAIT; + 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.'; - - EXEC msdb.dbo.sp_add_jobserver @job_name = sp_AllNightLog_PollForNewDatabases; + EXECUTE(@StringToExecute); + END; + END; - - - RAISERROR('Attaching schedule', 0, 1) WITH NOWAIT; - - - EXEC msdb.dbo.sp_attach_schedule @job_name = sp_AllNightLog_PollForNewDatabases, - @schedule_name = ten_seconds; - - - END; - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 36 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 36) WITH NOWAIT; - /* - - Look for Restore Pollster job -- this job sets up our watcher for new databases to back up - - */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 36 AS CheckID , + 150 AS Priority , + 'Performance' AS FindingsGroup , + 'Slow Storage Reads on Drive ' + + UPPER(LEFT(mf.physical_name, 1)) AS Finding , + 'https://www.brentozar.com/go/slow' AS URL , + 'Reads are averaging longer than 200ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details + FROM sys.dm_io_virtual_file_stats(NULL, NULL) + AS fs + INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id + AND fs.[file_id] = mf.[file_id] + WHERE ( io_stall_read_ms / ( 1.0 + num_of_reads ) ) > 200 + AND num_of_reads > 100000; + END; - - RAISERROR('Checking for restore pollster job', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 37 ) + BEGIN - - IF NOT EXISTS ( - SELECT 1 - FROM msdb.dbo.sysjobs - WHERE name = 'sp_AllNightLog_PollDiskForNewDatabases' - ) - - - BEGIN - - - RAISERROR('Creating restore pollster job', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 37) WITH NOWAIT; - - IF @EnableRestoreJobs = 1 - BEGIN - EXEC msdb.dbo.sp_add_job @job_name = sp_AllNightLog_PollDiskForNewDatabases, - @description = 'This is a worker for the purposes of polling your restore path for new entries to insert to the worker queue table.', - @category_name = 'Database Maintenance', - @owner_login_name = 'sa', - @enabled = 1; - END - ELSE - BEGIN - EXEC msdb.dbo.sp_add_job @job_name = sp_AllNightLog_PollDiskForNewDatabases, - @description = 'This is a worker for the purposes of polling your restore path for new entries to insert to the worker queue table.', - @category_name = 'Database Maintenance', - @owner_login_name = 'sa', - @enabled = 0; - END - - - - RAISERROR('Adding restore job step', 0, 1) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 37 AS CheckID , + 150 AS Priority , + 'Performance' AS FindingsGroup , + 'Slow Storage Writes on Drive ' + + UPPER(LEFT(mf.physical_name, 1)) AS Finding , + 'https://www.brentozar.com/go/slow' AS URL , + 'Writes are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details + FROM sys.dm_io_virtual_file_stats(NULL, NULL) + AS fs + INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id + AND fs.[file_id] = mf.[file_id] + WHERE ( io_stall_write_ms / ( 1.0 + + num_of_writes ) ) > 100 + AND num_of_writes > 100000; + END; - - EXEC msdb.dbo.sp_add_jobstep @job_name = sp_AllNightLog_PollDiskForNewDatabases, - @step_name = sp_AllNightLog_PollDiskForNewDatabases, - @subsystem = 'TSQL', - @command = 'EXEC sp_AllNightLog @PollDiskForNewDatabases = 1'; - - - - RAISERROR('Adding restore job server', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 40 ) + BEGIN + IF ( SELECT COUNT(*) + FROM tempdb.sys.database_files + WHERE type_desc = 'ROWS' + ) = 1 + BEGIN - - EXEC msdb.dbo.sp_add_jobserver @job_name = sp_AllNightLog_PollDiskForNewDatabases; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 40) WITH NOWAIT; - - - RAISERROR('Attaching schedule', 0, 1) WITH NOWAIT; - - - EXEC msdb.dbo.sp_attach_schedule @job_name = sp_AllNightLog_PollDiskForNewDatabases, - @schedule_name = ten_seconds; - - - END; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( 40 , + 'tempdb' , + 170 , + 'File Configuration' , + 'TempDB Only Has 1 Data File' , + 'https://www.brentozar.com/go/tempdb' , + 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' + ); + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 183 ) + BEGIN - /* - - This section creates @Jobs (quantity) of worker jobs to take log backups with + IF ( SELECT COUNT (distinct [size]) + FROM tempdb.sys.database_files + WHERE type_desc = 'ROWS' + HAVING MAX((size * 8) / (1024. * 1024)) - MIN((size * 8) / (1024. * 1024)) > 1. + ) <> 1 + BEGIN - They work in a queue + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 183) WITH NOWAIT; - It's queuete - - */ + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( 183 , + 'tempdb' , + 170 , + 'File Configuration' , + 'TempDB Unevenly Sized Data Files' , + 'https://www.brentozar.com/go/tempdb' , + 'TempDB data files are not configured with the same size. Unevenly sized tempdb data files will result in unevenly sized workloads.' + ); + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 44 ) + BEGIN - RAISERROR('Checking for sp_AllNightLog backup jobs', 0, 1) WITH NOWAIT; - - - SELECT @counter = COUNT(*) + 1 - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp[_]AllNightLog[_]Backup[_]%'; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 44) WITH NOWAIT; - SET @msg = 'Found ' + CONVERT(NVARCHAR(10), (@counter - 1)) + ' backup jobs -- ' + CASE WHEN @counter < @Jobs THEN + 'starting loop!' - WHEN @counter >= @Jobs THEN 'skipping loop!' - ELSE 'Oh woah something weird happened!' - END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 44 AS CheckID , + 150 AS Priority , + 'Performance' AS FindingsGroup , + 'Queries Forcing Order Hints' AS Finding , + 'https://www.brentozar.com/go/hints' AS URL , + CAST(occurrence AS VARCHAR(10)) + + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details + FROM sys.dm_exec_query_optimizer_info + WHERE counter = 'order hint' + AND occurrence > 1000; + END; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 45 ) + BEGIN - - WHILE @counter <= @Jobs + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 45) WITH NOWAIT; - - BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 45 AS CheckID , + 150 AS Priority , + 'Performance' AS FindingsGroup , + 'Queries Forcing Join Hints' AS Finding , + 'https://www.brentozar.com/go/hints' AS URL , + CAST(occurrence AS VARCHAR(10)) + + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details + FROM sys.dm_exec_query_optimizer_info + WHERE counter = 'join hint' + AND occurrence > 1000; + END; - - RAISERROR('Setting job name', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 49 ) + BEGIN - SET @job_name_backups = N'sp_AllNightLog_Backup_' + CASE WHEN @counter < 10 THEN N'0' + CONVERT(NVARCHAR(10), @counter) - WHEN @counter >= 10 THEN CONVERT(NVARCHAR(10), @counter) - END; - - - RAISERROR('Setting @job_sql', 0, 1) WITH NOWAIT; - - - SET @job_sql = N' - - EXEC msdb.dbo.sp_add_job @job_name = ' + @job_name_backups + ', - @description = ' + @job_description_backups + ', - @category_name = ' + @job_category + ', - @owner_login_name = ' + @job_owner + ','; - IF @EnableBackupJobs = 1 - BEGIN - SET @job_sql = @job_sql + ' @enabled = 1; '; - END - ELSE - BEGIN - SET @job_sql = @job_sql + ' @enabled = 0; '; - END - - - SET @job_sql = @job_sql + ' - EXEC msdb.dbo.sp_add_jobstep @job_name = ' + @job_name_backups + ', - @step_name = ' + @job_name_backups + ', - @subsystem = ''TSQL'', - @command = ' + @job_command_backups + '; - - - EXEC msdb.dbo.sp_add_jobserver @job_name = ' + @job_name_backups + '; - - - EXEC msdb.dbo.sp_attach_schedule @job_name = ' + @job_name_backups + ', - @schedule_name = ten_seconds; - - '; - - - SET @counter += 1; - - - IF @Debug = 1 - BEGIN - RAISERROR(@job_sql, 0, 1) WITH NOWAIT; - END; - - - IF @job_sql IS NULL - BEGIN - RAISERROR('@job_sql is NULL for some reason', 0, 1) WITH NOWAIT; - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 49) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 49 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Linked Server Configured' AS Finding , + 'https://www.brentozar.com/go/link' AS URL , + +CASE WHEN l.remote_name = 'sa' + THEN COALESCE(s.data_source, s.provider) + + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' + ELSE COALESCE(s.data_source, s.provider) + + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' + END AS Details + FROM sys.servers s + INNER JOIN sys.linked_logins l ON s.server_id = l.server_id + WHERE s.is_linked = 1; + END; - EXEC sp_executesql @job_sql; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 50 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN - - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 50) WITH NOWAIT; + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 50 AS CheckID , + 100 AS Priority , + ''Performance'' AS FindingsGroup , + ''Max Memory Set Too High'' AS Finding , + ''https://www.brentozar.com/go/max'' AS URL , + ''SQL Server max memory is set to '' + + CAST(c.value_in_use AS VARCHAR(20)) + + '' megabytes, but the server only has '' + + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + + '' megabytes. SQL Server may drain the system dry of memory, and under certain conditions, this can cause Windows to swap to disk.'' AS Details + FROM sys.dm_os_sys_memory m + INNER JOIN sys.configurations c ON c.name = ''max server memory (MB)'' + WHERE CAST(m.total_physical_memory_kb AS BIGINT) < ( CAST(c.value_in_use AS BIGINT) * 1024 ) 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.'; - /* - - This section creates @Jobs (quantity) of worker jobs to restore logs with + EXECUTE(@StringToExecute); + END; + END; - They too work in a queue + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 51 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN - Like a queue-t 3.14 - - */ + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 51) WITH NOWAIT + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 51 AS CheckID , + 1 AS Priority , + ''Performance'' AS FindingsGroup , + ''Memory Dangerously Low'' AS Finding , + ''https://www.brentozar.com/go/max'' AS URL , + ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details + FROM sys.dm_os_sys_memory m + WHERE CAST(m.available_physical_memory_kb AS BIGINT) < 262144 OPTION (RECOMPILE);'; - RAISERROR('Checking for sp_AllNightLog Restore jobs', 0, 1) WITH NOWAIT; - - - SELECT @counter = COUNT(*) + 1 - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp[_]AllNightLog[_]Restore[_]%'; + 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.'; - SET @msg = 'Found ' + CONVERT(NVARCHAR(10), (@counter - 1)) + ' restore jobs -- ' + CASE WHEN @counter < @Jobs THEN + 'starting loop!' - WHEN @counter >= @Jobs THEN 'skipping loop!' - ELSE 'Oh woah something weird happened!' - END; + EXECUTE(@StringToExecute); + END; + END; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 159 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN - - WHILE @counter <= @Jobs + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 159) WITH NOWAIT; - - BEGIN + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 159 AS CheckID , + 1 AS Priority , + ''Performance'' AS FindingsGroup , + ''Memory Dangerously Low in NUMA Nodes'' AS Finding , + ''https://www.brentozar.com/go/max'' AS URL , + ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details + FROM sys.dm_os_nodes m + WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%'' OPTION (RECOMPILE);'; - - RAISERROR('Setting job name', 0, 1) WITH NOWAIT; + 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.'; - SET @job_name_restores = N'sp_AllNightLog_Restore_' + CASE WHEN @counter < 10 THEN N'0' + CONVERT(NVARCHAR(10), @counter) - WHEN @counter >= 10 THEN CONVERT(NVARCHAR(10), @counter) - END; - - - RAISERROR('Setting @job_sql', 0, 1) WITH NOWAIT; + EXECUTE(@StringToExecute); + END; + END; - - SET @job_sql = N' - - EXEC msdb.dbo.sp_add_job @job_name = ' + @job_name_restores + ', - @description = ' + @job_description_restores + ', - @category_name = ' + @job_category + ', - @owner_login_name = ' + @job_owner + ','; - IF @EnableRestoreJobs = 1 - BEGIN - SET @job_sql = @job_sql + ' @enabled = 1; '; - END - ELSE - BEGIN - SET @job_sql = @job_sql + ' @enabled = 0; '; - END - - - SET @job_sql = @job_sql + ' - - EXEC msdb.dbo.sp_add_jobstep @job_name = ' + @job_name_restores + ', - @step_name = ' + @job_name_restores + ', - @subsystem = ''TSQL'', - @command = ' + @job_command_restores + '; - - - EXEC msdb.dbo.sp_add_jobserver @job_name = ' + @job_name_restores + '; - - - EXEC msdb.dbo.sp_attach_schedule @job_name = ' + @job_name_restores + ', - @schedule_name = ten_seconds; - - '; - - - SET @counter += 1; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 53 ) + BEGIN - - IF @Debug = 1 - BEGIN - RAISERROR(@job_sql, 0, 1) WITH NOWAIT; - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 53) WITH NOWAIT; - - IF @job_sql IS NULL - BEGIN - RAISERROR('@job_sql is NULL for some reason', 0, 1) WITH NOWAIT; - END; + DECLARE @AOFCI AS INT, @AOAG AS INT, @HAType AS VARCHAR(10), @errmsg AS VARCHAR(200) + + SELECT @AOAG = CAST(SERVERPROPERTY('IsHadrEnabled') AS INT) + SELECT @AOFCI = CAST(SERVERPROPERTY('IsClustered') AS INT) + + IF (SELECT COUNT(DISTINCT join_state_desc) FROM sys.dm_hadr_availability_replica_cluster_states) = 2 + BEGIN --if count is 2 both JOINED_STANDALONE and JOINED_FCI is configured + SET @AOFCI = 1 + END + + SELECT @HAType = + CASE + WHEN @AOFCI = 1 AND @AOAG =1 THEN 'FCIAG' + WHEN @AOFCI = 1 AND @AOAG =0 THEN 'FCI' + WHEN @AOFCI = 0 AND @AOAG =1 THEN 'AG' + ELSE 'STANDALONE' + END + + IF (@HAType IN ('FCIAG','FCI','AG')) + BEGIN + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node' AS Finding , + 'https://BrentOzar.com/go/node' AS URL , + 'This is a node in a cluster.' AS Details + + IF @HAType = 'AG' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + CASE ar.replica_server_name WHEN dhags.primary_replica THEN 'PRIMARY' + ELSE 'SECONDARY' + END + '=' + UPPER(ar.replica_server_name) + FROM sys.availability_groups AS ag + LEFT OUTER JOIN sys.availability_replicas AS ar ON ag.group_id = ar.group_id + LEFT OUTER JOIN sys.dm_hadr_availability_group_states as dhags ON ag.group_id = dhags.group_id + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn Availability Group (AG)' As Details + END + + IF @HAType = 'FCI' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + CASE is_current_owner + WHEN 1 THEN 'PRIMARY' + ELSE 'SECONDARY' + END + '=' + UPPER(NodeName) + FROM sys.dm_os_cluster_nodes + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn Failover Cluster Instance (FCI)' As Details + END + + IF @HAType = 'FCIAG' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + HighAvailabilityRoleDesc + '=' + ServerName + FROM (SELECT UPPER(ar.replica_server_name) AS ServerName + ,CASE ar.replica_server_name WHEN dhags.primary_replica THEN 'PRIMARY' + ELSE 'SECONDARY' + END AS HighAvailabilityRoleDesc + FROM sys.availability_groups AS ag + LEFT OUTER JOIN sys.availability_replicas AS ar ON ag.group_id = ar.group_id + LEFT OUTER JOIN sys.dm_hadr_availability_group_states as dhags ON ag.group_id = dhags.group_id + WHERE UPPER(CAST(SERVERPROPERTY('ServerName') AS VARCHAR)) <> ar.replica_server_name + UNION ALL + SELECT UPPER(NodeName) AS ServerName + ,CASE is_current_owner + WHEN 1 THEN 'PRIMARY' + ELSE 'SECONDARY' + END AS HighAvailabilityRoleDesc + FROM sys.dm_os_cluster_nodes) AS Z + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn FCI with AG (Failover Cluster Instance with Availability Group).'As Details + END + END + END; - EXEC sp_executesql @job_sql; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 55 ) + BEGIN - - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; + IF @UsualDBOwner IS NULL + SET @UsualDBOwner = SUSER_SNAME(0x01); - RAISERROR('Setup complete!', 0, 1) WITH NOWAIT; - - END; --End for the Agent job creation + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 55 AS CheckID , + [name] AS DatabaseName , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Database Owner <> ' + @UsualDBOwner AS Finding , + 'https://www.brentozar.com/go/owndb' AS URL , + ( 'Database name: ' + [name] + ' ' + + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details + FROM sys.databases + WHERE (((SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01)) AND (name IN (N'master', N'model', N'msdb', N'tempdb'))) + OR ((SUSER_SNAME(owner_sid) <> @UsualDBOwner) AND (name NOT IN (N'master', N'model', N'msdb', N'tempdb'))) + ) + AND name NOT IN ( SELECT DISTINCT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 55); + END; - END;--End for Database and Table creation + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 213 ) + BEGIN - END TRY + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 213) WITH NOWAIT; - BEGIN CATCH + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 213 AS CheckID , + [name] AS DatabaseName , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Database Owner is Unknown' AS Finding , + '' AS URL , + ( 'Database name: ' + [name] + ' ' + + 'Owner name: ' + ISNULL(SUSER_SNAME(owner_sid),'~~ UNKNOWN ~~') ) AS Details + FROM sys.databases + WHERE SUSER_SNAME(owner_sid) is NULL + AND name NOT IN ( SELECT DISTINCT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 213); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 57 ) + BEGIN - SELECT @msg = N'Error occurred during setup: ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 57) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 57 AS CheckID , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'SQL Agent Job Runs at Startup' AS Finding , + 'https://www.brentozar.com/go/startup' AS URL , + ( 'Job [' + j.name + + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details + FROM msdb.dbo.sysschedules sched + JOIN msdb.dbo.sysjobschedules jsched ON sched.schedule_id = jsched.schedule_id + JOIN msdb.dbo.sysjobs j ON jsched.job_id = j.job_id + WHERE sched.freq_type = 64 + AND sched.enabled = 1; + END; - WHILE @@TRANCOUNT > 0 - ROLLBACK; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 97 ) + BEGIN - END CATCH; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 97) WITH NOWAIT; -END; /* IF @RunSetup = 1 */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 97 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Unusual SQL Server Edition' AS Finding , + 'https://www.brentozar.com/go/workgroup' AS URL , + ( 'This server is using ' + + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + + ', which is capped at low amounts of CPU and memory.' ) AS Details + WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Data Center%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Business Intelligence%'; + END; -RETURN; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 154 ) + AND SERVERPROPERTY('EngineEdition') <> 8 + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 154) WITH NOWAIT; -UpdateConfigs: + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 154 AS CheckID , + 10 AS Priority , + 'Performance' AS FindingsGroup , + '32-bit SQL Server Installed' AS Finding , + 'https://www.brentozar.com/go/32bit' AS URL , + ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details + WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%'; + END; -IF @UpdateSetup = 1 - - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 62 ) + BEGIN - /* If we're enabling backup jobs, we may need to run restore with recovery on msdbCentral to bring it online: */ - IF @EnableBackupJobs = 1 AND EXISTS (SELECT * FROM sys.databases WHERE name = 'msdbCentral' AND state = 1) - BEGIN - RAISERROR('msdbCentral exists, but is in restoring state. Running restore with recovery...', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 62) WITH NOWAIT; - BEGIN TRY - RESTORE DATABASE [msdbCentral] WITH RECOVERY; - END TRY - - BEGIN CATCH + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 62 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Performance' AS FindingsGroup , + 'Old Compatibility Level' AS Finding , + 'https://www.brentozar.com/go/compatlevel' AS URL , + ( 'Database ' + [name] + + ' is compatibility level ' + + CAST(compatibility_level AS VARCHAR(20)) + + ', which may cause unwanted results when trying to run queries that have newer T-SQL features.' ) AS Details + FROM sys.databases + WHERE name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 62) + AND compatibility_level <= 90; + END; - SELECT @error_number = ERROR_NUMBER(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 94 ) + BEGIN - SELECT @msg = N'Error running restore with recovery on msdbCentral, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; - END CATCH; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 94 AS CheckID , + 200 AS [Priority] , + 'Monitoring' AS FindingsGroup , + '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 + FROM msdb.[dbo].[sysjobs] j + WHERE j.enabled = 1 + AND j.notify_email_operator_id = 0 + AND j.notify_netsend_operator_id = 0 + AND j.notify_page_operator_id = 0 + AND j.category_id <> 100; /* Exclude SSRS category */ + END; - END + IF EXISTS ( SELECT 1 + FROM sys.configurations + WHERE name = 'remote admin connections' + AND value_in_use = 0 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 100 ) + BEGIN - /* Only check for this after trying to restore msdbCentral: */ - IF @EnableBackupJobs = 1 AND NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'msdbCentral' AND state = 0) - BEGIN - RAISERROR('msdbCentral is not online. Repair that first, then try to enable backup jobs.', 0, 1) WITH NOWAIT; - RETURN - END + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 100) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 100 AS CheckID , + 170 AS Priority , + 'Reliability' AS FindingGroup , + 'Remote DAC Disabled' AS Finding , + 'https://www.brentozar.com/go/dac' AS URL , + 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.'; + END; - IF OBJECT_ID('msdbCentral.dbo.backup_configuration') IS NOT NULL + IF EXISTS ( SELECT * + FROM sys.dm_os_schedulers + WHERE is_online = 0 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 101 ) + BEGIN - RAISERROR('Found backup config, checking variables...', 0, 1) WITH NOWAIT; - - BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 101) WITH NOWAIT; - BEGIN TRY + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 101 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'CPU Schedulers Offline' AS Finding , + 'https://www.brentozar.com/go/schedulers' AS URL , + 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.'; + END; - - IF @RPOSeconds IS NOT NULL + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 110 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_os_memory_nodes') + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 110) WITH NOWAIT; - BEGIN + SET @StringToExecute = 'IF EXISTS (SELECT * + FROM sys.dm_os_nodes n + INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id + WHERE n.node_state_desc = ''OFFLINE'') + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 110 AS CheckID , + 50 AS Priority , + ''Performance'' AS FindingGroup , + ''Memory Nodes Offline'' AS Finding , + ''https://www.brentozar.com/go/schedulers'' AS URL , + ''Due to affinity masking or licensing problems, some of the memory may not be available.'' 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.'; + + EXECUTE(@StringToExecute); + END; - RAISERROR('Attempting to update RPO setting', 0, 1) WITH NOWAIT; + IF EXISTS ( SELECT * + FROM sys.databases + WHERE state > 1 ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 102 ) + BEGIN - UPDATE c - SET c.configuration_setting = CONVERT(NVARCHAR(10), @RPOSeconds) - FROM msdbCentral.dbo.backup_configuration AS c - WHERE c.configuration_name = N'log backup frequency'; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 102) WITH NOWAIT; - END; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 102 AS CheckID , + [name] , + 20 AS Priority , + 'Reliability' AS FindingGroup , + 'Unusual Database State: ' + [state_desc] AS Finding , + 'https://www.brentozar.com/go/repair' AS URL , + 'This database may not be online.' + FROM sys.databases + WHERE state > 1; + END; - - IF @BackupPath IS NOT NULL + IF EXISTS ( SELECT * + FROM master.sys.extended_procedures ) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 105 ) + BEGIN - BEGIN - - RAISERROR('Attempting to update Backup Path setting', 0, 1) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 105) WITH NOWAIT; - UPDATE c - SET c.configuration_setting = @BackupPath - FROM msdbCentral.dbo.backup_configuration AS c - WHERE c.configuration_name = N'log backup path'; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 105 AS CheckID , + 'master' , + 200 AS Priority , + 'Reliability' AS FindingGroup , + 'Extended Stored Procedures in Master' AS Finding , + 'https://www.brentozar.com/go/clr' AS URL , + 'The [' + name + + '] extended stored procedure is in the master database. CLR may be in use, and the master database now needs to be part of your backup/recovery planning.' + FROM master.sys.extended_procedures; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 107 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 107) WITH NOWAIT; - END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 107 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: ' + wait_type AS Finding , + 'https://www.brentozar.com/go/poison/#' + wait_type AS URL , + CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'POOL_LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') + GROUP BY wait_type + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + AND SUM([wait_time_ms]) > 60000; + END; - END TRY + 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; - BEGIN CATCH + 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; - SELECT @error_number = ERROR_NUMBER(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - SELECT @msg = N'Error updating backup configuration setting, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; - END CATCH; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 121 ) + BEGIN - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 121) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 121 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: Serializable Locking' AS Finding , + 'https://www.brentozar.com/go/serializable' AS URL , + CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + AND SUM([wait_time_ms]) > 60000; + END; - IF OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL - RAISERROR('Found restore config, checking variables...', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 111 ) + BEGIN - BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 111) WITH NOWAIT; - BEGIN TRY + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + DatabaseName , + URL , + Details + ) + SELECT 111 AS CheckID , + 50 AS Priority , + 'Reliability' AS FindingGroup , + 'Possibly Broken Log Shipping' AS Finding , + d.[name] , + 'https://www.brentozar.com/go/shipping' AS URL , + d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' + FROM [master].sys.databases d + INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id + AND dm.mirroring_role IS NULL + WHERE ( d.[state] = 1 + OR (d.[state] = 0 AND d.[is_in_standby] = 1) ) + AND NOT EXISTS(SELECT * FROM msdb.dbo.restorehistory rh + INNER JOIN msdb.dbo.backupset bs ON rh.backup_set_id = bs.backup_set_id + WHERE d.[name] COLLATE SQL_Latin1_General_CP1_CI_AS = rh.destination_database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND rh.restore_date >= DATEADD(dd, -2, GETDATE())); - EXEC msdb.dbo.sp_update_schedule @name = ten_seconds, @active_start_date = @active_start_date, @active_start_time = 000000; + END; - IF @EnableRestoreJobs IS NOT NULL - BEGIN - RAISERROR('Changing restore job status based on @EnableBackupJobs parameter...', 0, 1) WITH NOWAIT; - INSERT INTO @jobs_to_change(name) - SELECT name - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Restore%' OR name = 'sp_AllNightLog_PollDiskForNewDatabases'; - DECLARE jobs_cursor CURSOR FOR - SELECT name - FROM @jobs_to_change - - OPEN jobs_cursor - FETCH NEXT FROM jobs_cursor INTO @current_job_name - - WHILE @@FETCH_STATUS = 0 - BEGIN - RAISERROR(@current_job_name, 0, 1) WITH NOWAIT; - EXEC msdb.dbo.sp_update_job @job_name=@current_job_name,@enabled = @EnableRestoreJobs; - FETCH NEXT FROM jobs_cursor INTO @current_job_name - END - - CLOSE jobs_cursor - DEALLOCATE jobs_cursor - DELETE @jobs_to_change; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 112 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'change_tracking_databases') + BEGIN - /* If they wanted to turn off restore jobs, wait to make sure that finishes before we start enabling the backup jobs */ - IF @EnableRestoreJobs = 0 - BEGIN - SET @started_waiting_for_jobs = GETDATE(); - SELECT @counter = COUNT(*) - FROM [msdb].[dbo].[sysjobactivity] [ja] - INNER JOIN [msdb].[dbo].[sysjobs] [j] - ON [ja].[job_id] = [j].[job_id] - WHERE [ja].[session_id] = ( - SELECT TOP 1 [session_id] - FROM [msdb].[dbo].[syssessions] - ORDER BY [agent_start_date] DESC - ) - AND [start_execution_date] IS NOT NULL - AND [stop_execution_date] IS NULL - AND [j].[name] LIKE 'sp_AllNightLog_Restore%'; - - WHILE @counter > 0 - BEGIN - IF DATEADD(SS, 120, @started_waiting_for_jobs) < GETDATE() - BEGIN - RAISERROR('OH NOES! We waited 2 minutes and restore jobs are still running. We are stopping here - get a meatbag involved to figure out if restore jobs need to be killed, and the backup jobs will need to be enabled manually.', 16, 1) WITH NOWAIT; - RETURN - END - SET @msg = N'Waiting for ' + CAST(@counter AS NVARCHAR(100)) + N' sp_AllNightLog_Restore job(s) to finish.' - RAISERROR(@msg, 0, 1) WITH NOWAIT; - WAITFOR DELAY '0:00:01'; -- Wait until the restore jobs are fully stopped - - SELECT @counter = COUNT(*) - FROM [msdb].[dbo].[sysjobactivity] [ja] - INNER JOIN [msdb].[dbo].[sysjobs] [j] - ON [ja].[job_id] = [j].[job_id] - WHERE [ja].[session_id] = ( - SELECT TOP 1 [session_id] - FROM [msdb].[dbo].[syssessions] - ORDER BY [agent_start_date] DESC - ) - AND [start_execution_date] IS NOT NULL - AND [stop_execution_date] IS NULL - AND [j].[name] LIKE 'sp_AllNightLog_Restore%'; - END - END /* IF @EnableRestoreJobs = 0 */ + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 112) WITH NOWAIT; + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + DatabaseName, + URL, + Details) + SELECT 112 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Change Tracking Enabled'' AS Finding, + d.[name], + ''https://www.brentozar.com/go/tracking'' AS URL, + ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id 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.'; + + EXECUTE(@StringToExecute); + END; - IF @EnableBackupJobs IS NOT NULL - BEGIN - RAISERROR('Changing backup job status based on @EnableBackupJobs parameter...', 0, 1) WITH NOWAIT; - INSERT INTO @jobs_to_change(name) - SELECT name - FROM msdb.dbo.sysjobs - WHERE name LIKE 'sp_AllNightLog_Backup%' OR name = 'sp_AllNightLog_PollForNewDatabases'; - DECLARE jobs_cursor CURSOR FOR - SELECT name - FROM @jobs_to_change - - OPEN jobs_cursor - FETCH NEXT FROM jobs_cursor INTO @current_job_name - - WHILE @@FETCH_STATUS = 0 - BEGIN - RAISERROR(@current_job_name, 0, 1) WITH NOWAIT; - EXEC msdb.dbo.sp_update_job @job_name=@current_job_name,@enabled = @EnableBackupJobs; - FETCH NEXT FROM jobs_cursor INTO @current_job_name - END - - CLOSE jobs_cursor - DEALLOCATE jobs_cursor - DELETE @jobs_to_change; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 116 ) + AND EXISTS (SELECT * FROM msdb.sys.all_columns WHERE name = 'compressed_backup_size') + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 116) WITH NOWAIT - - IF @RTOSeconds IS NOT NULL + SET @StringToExecute = 'INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 116 AS CheckID , + 200 AS Priority , + ''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 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);'; + + 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.'; + + EXECUTE(@StringToExecute); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 117 ) + AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_exec_query_resource_semaphores') BEGIN - - RAISERROR('Attempting to update RTO setting', 0, 1) WITH NOWAIT; - - UPDATE c - SET c.configuration_setting = CONVERT(NVARCHAR(10), @RTOSeconds) - FROM msdb.dbo.restore_configuration AS c - WHERE c.configuration_name = N'log restore frequency'; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 117) WITH NOWAIT; + + SET @StringToExecute = 'IF 0 < (SELECT SUM([forced_grant_count]) FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL) + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 117 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Pressure Affecting Queries'' AS Finding, + ''https://www.brentozar.com/go/grants'' AS URL, + CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' + FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT 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.'; + + EXECUTE(@StringToExecute); END; - - IF @RestorePath IS NOT NULL - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 124 ) BEGIN - RAISERROR('Attempting to update Restore Path setting', 0, 1) WITH NOWAIT; - - UPDATE c - SET c.configuration_setting = @RestorePath - FROM msdb.dbo.restore_configuration AS c - WHERE c.configuration_name = N'log restore path'; - - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 124) WITH NOWAIT; + + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 124, + 150, + 'Performance', + 'Deadlocks Happening Daily', + 'https://www.brentozar.com/go/deadlocks', + CAST(CAST(p.cntr_value / @DaysUptime AS BIGINT) AS NVARCHAR(100)) + ' average deadlocks per day. To find them, run sp_BlitzLock.' AS Details + FROM sys.dm_os_performance_counters p + INNER JOIN sys.databases d ON d.name = 'tempdb' + WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' + AND RTRIM(p.instance_name) = '_Total' + AND p.cntr_value > 0 + AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; END; - END TRY - - - BEGIN CATCH - + IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 125 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 125) WITH NOWAIT; + + DECLARE @user_perm_sql NVARCHAR(MAX) = N''; + DECLARE @user_perm_gb_out DECIMAL(38,2); + + IF @ProductVersionMajor >= 11 + + BEGIN + + SET @user_perm_sql += N' + SELECT @user_perm_gb = CASE WHEN (pages_kb / 1024. / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), (pages_kb / 1024. / 1024.)) + ELSE NULL + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'' + '; + + END + + IF @ProductVersionMajor < 11 + + BEGIN + SET @user_perm_sql += N' + SELECT @user_perm_gb = CASE WHEN ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.)) + ELSE NULL + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'' + '; + + END - SELECT @error_number = ERROR_NUMBER(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); + EXEC sys.sp_executesql @user_perm_sql, + N'@user_perm_gb DECIMAL(38,2) OUTPUT', + @user_perm_gb = @user_perm_gb_out OUTPUT - SELECT @msg = N'Error updating restore configuration setting, error number is ' + CONVERT(NVARCHAR(10), ERROR_NUMBER()) + ', error message is ' + ERROR_MESSAGE(), - @error_severity = ERROR_SEVERITY(), - @error_state = ERROR_STATE(); - - RAISERROR(@msg, @error_severity, @error_state) WITH NOWAIT; + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://www.brentozar.com/askbrent/plan-cache-erased-recently/', + 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + + CASE WHEN @user_perm_gb_out IS NULL + THEN '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' + ELSE '. You also have ' + CONVERT(NVARCHAR(20), @user_perm_gb_out) + ' GB of USERSTORE_TOKENPERM, which could indicate unusual memory consumption.' + END + FROM sys.dm_exec_query_stats WITH (NOLOCK) + ORDER BY creation_time; + END; + IF EXISTS (SELECT * FROM sys.configurations WHERE name = 'priority boost' AND (value = 1 OR value_in_use = 1)) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 126 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 126) WITH NOWAIT; + + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://www.brentozar.com/go/priorityboost/', + 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.'); + END; - END CATCH; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 128 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN - END; + IF (@ProductVersionMajor = 15 AND @ProductVersionMinor < 2000) OR + (@ProductVersionMajor = 14 AND @ProductVersionMinor < 1000) OR + (@ProductVersionMajor = 13 AND @ProductVersionMinor < 6300) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor < 6024) OR + (@ProductVersionMajor = 11 /*AND @ProductVersionMinor < 7001)*/) OR + (@ProductVersionMajor = 10.5 /*AND @ProductVersionMinor < 6000*/) OR + (@ProductVersionMajor = 10 /*AND @ProductVersionMinor < 6000*/) OR + (@ProductVersionMajor = 9 /*AND @ProductVersionMinor <= 5000*/) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 128) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://www.brentozar.com/go/unsupported', + 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + + CASE WHEN @ProductVersionMajor >= 12 THEN + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' + ELSE ' is no longer supported by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); + END; - RAISERROR('Update complete!', 0, 1) WITH NOWAIT; + END; + + /* Reliability - Dangerous Build of SQL Server (Corruption) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 129 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN + IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2342) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 129) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(129, 20, 'Reliability', 'Dangerous Build of SQL Server (Corruption)', 'http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds', + 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); + END; - RETURN; + END; - END; --End updates to configuration table + /* Reliability - Dangerous Build of SQL Server (Security) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 157 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN + IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR + (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR + (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4000 AND @ProductVersionMinor <= 4017) OR + (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4251 AND @ProductVersionMinor <= 4319) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3129) OR + (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3300 AND @ProductVersionMinor <= 3447) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2253) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2300 AND @ProductVersionMinor <= 2370) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 157) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(157, 20, 'Reliability', 'Dangerous Build of SQL Server (Security)', 'https://technet.microsoft.com/en-us/library/security/MS14-044', + 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); + END; + END; + + /* Check if SQL 2016 Standard Edition but not SP1 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 189 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN + IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 189) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(189, 100, 'Features', 'Missing Features', 'https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-server-2016-service-pack-1-sp1-released/', + 'SQL 2016 Standard Edition is being used but not Service Pack 1. Check the URL for a list of Enterprise Features that are included in Standard Edition as of SP1.'); + END; -END; -- Final END for stored proc -GO - -IF OBJECT_ID('dbo.sp_Blitz') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0;'); -GO - -ALTER PROCEDURE [dbo].[sp_Blitz] - @Help TINYINT = 0 , - @CheckUserDatabaseObjects TINYINT = 1 , - @CheckProcedureCache TINYINT = 0 , - @OutputType VARCHAR(20) = 'TABLE' , - @OutputProcedureCache TINYINT = 0 , - @CheckProcedureCacheFilter VARCHAR(10) = NULL , - @CheckServerInfo TINYINT = 0 , - @SkipChecksServer NVARCHAR(256) = NULL , - @SkipChecksDatabase NVARCHAR(256) = NULL , - @SkipChecksSchema NVARCHAR(256) = NULL , - @SkipChecksTable NVARCHAR(256) = NULL , - @IgnorePrioritiesBelow INT = NULL , - @IgnorePrioritiesAbove INT = NULL , - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @OutputXMLasNVARCHAR TINYINT = 0 , - @EmailRecipients VARCHAR(MAX) = NULL , - @EmailProfile sysname = NULL , - @SummaryMode TINYINT = 0 , - @BringThePain TINYINT = 0 , - @Debug TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; - SET @OutputType = UPPER(@OutputType); - - IF @Help = 1 PRINT ' - /* - sp_Blitz from http://FirstResponderKit.org - - This script checks the health of your SQL Server and gives you a prioritized - to-do list of the most urgent things you should consider fixing. + END; + + /* Check if SQL 2017 but not CU3 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 216 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN + IF (@ProductVersionMajor = 14 AND @ProductVersionMinor < 3015) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 216) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(216, 100, 'Features', 'Missing Features', 'https://support.microsoft.com/en-us/help/4041814', + 'SQL 2017 is being used but not Cumulative Update 3. We''d recommend patching to take advantage of increased analytics when running BlitzCache.'); + END; - 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. + END; - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - If a database name has a question mark in it, some tests will fail. Gotta - love that unsupported sp_MSforeachdb. - - If you have offline databases, sp_Blitz fails the first time you run it, - but does work the second time. (Hoo, boy, this will be fun to debug.) - - @OutputServerName will output QueryPlans as NVARCHAR(MAX) since Microsoft - has refused to support XML columns in Linked Server queries. The bug is now - 16 years old! *~ \o/ ~* + /* Cumulative Update Available */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 217 ) + AND SERVERPROPERTY('EngineEdition') NOT IN (5,8) /* Azure Managed Instances and Azure SQL DB*/ + AND EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SqlServerVersions' AND TABLE_TYPE = 'BASE TABLE') + AND NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (128, 129, 157, 189, 216)) /* Other version checks */ + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 217) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 217, 100, 'Reliability', 'Cumulative Update Available', COALESCE(v.Url, 'https://SQLServerUpdates.com/'), + v.MinorVersionName + ' was released on ' + CAST(CONVERT(DATETIME, v.ReleaseDate, 112) AS VARCHAR(100)) + FROM dbo.SqlServerVersions v + WHERE v.MajorVersionNumber = @ProductVersionMajor + AND v.MinorVersionNumber > @ProductVersionMinor + ORDER BY v.MinorVersionNumber DESC; + END; - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) + /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 145 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_db_xtp_table_memory_stats' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 145) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 145 AS CheckID, + 10 AS Priority, + ''Performance'' AS FindingsGroup, + ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, + ''https://www.brentozar.com/go/hekaton'' AS URL, + CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details + FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' + WHERE c.name = ''max server memory (MB)'' + GROUP BY c.value_in_use + HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) + OR SUM(mem.pages_kb / 1024.0) > 250000 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.'; + + EXECUTE(@StringToExecute); + END; - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + /* Performance - In-Memory OLTP (Hekaton) In Use */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 146 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_db_xtp_table_memory_stats' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 146) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 146 AS CheckID, + 200 AS Priority, + ''Performance'' AS FindingsGroup, + ''In-Memory OLTP (Hekaton) In Use'' AS Finding, + ''https://www.brentozar.com/go/hekaton'' AS URL, + CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS DECIMAL(6,1)) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details + FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' + WHERE c.name = ''max server memory (MB)'' + GROUP BY c.value_in_use + HAVING SUM(mem.pages_kb / 1024.0) > 1000 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.'; + + EXECUTE(@StringToExecute); + END; + /* In-Memory OLTP (Hekaton) - Transaction Errors */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 147 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_xtp_transaction_stats' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 147) WITH NOWAIT + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 147 AS CheckID, + 100 AS Priority, + ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, + ''Transaction Errors'' AS Finding, + ''https://www.brentozar.com/go/hekaton'' AS URL, + ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details + FROM sys.dm_xtp_transaction_stats + WHERE validation_failures <> 0 + OR dependencies_failed <> 0 + OR write_conflicts <> 0 + OR unique_constraint_violations <> 0 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.'; + + EXECUTE(@StringToExecute); + END; - Parameter explanations: + /* Reliability - Database Files on Network File Shares */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 148 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 148) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 148 AS CheckID , + d.[name] AS DatabaseName , + 170 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Files on Network File Shares' AS Finding , + 'https://www.brentozar.com/go/nas' AS URL , + ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details + FROM sys.databases d + INNER JOIN sys.master_files mf ON d.database_id = mf.database_id + WHERE mf.physical_name LIKE '\\%' + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 148); + END; - @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. - @CheckServerInfo 1=show server info like CPUs, memory, virtualization - @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. - @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm - @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' - @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list | ''SCHEMA''=version and field list | ''NONE'' = none - @IgnorePrioritiesBelow 50=ignore priorities below 50 - @IgnorePrioritiesAbove 50=ignore priorities above 50 - For the rest of the parameters, see https://www.BrentOzar.com/blitz/documentation for details. + /* Reliability - Database Files Stored in Azure */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 149 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 149) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 149 AS CheckID , + d.[name] AS DatabaseName , + 170 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Files Stored in Azure' AS Finding , + 'https://www.brentozar.com/go/azurefiles' AS URL , + ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details + FROM sys.databases d + INNER JOIN sys.master_files mf ON d.database_id = mf.database_id + WHERE mf.physical_name LIKE 'http://%' + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 149); + END; + /* Reliability - Errors Logged Recently in the Default Trace */ + /* First, let's check that there aren't any issues with the trace files */ + BEGIN TRY - MIT License - - Copyright for portions of sp_Blitz are held by Microsoft as part of project - tigertoolbox and are provided under the MIT license: - https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. + IF @SkipTrace = 0 + BEGIN + INSERT INTO #fnTraceGettable + ( TextData , + DatabaseName , + EventClass , + Severity , + StartTime , + EndTime , + Duration , + NTUserName , + NTDomainName , + HostName , + ApplicationName , + LoginName , + DBUserName + ) + SELECT TOP 20000 + CONVERT(NVARCHAR(4000),t.TextData) , + t.DatabaseName , + t.EventClass , + t.Severity , + t.StartTime , + t.EndTime , + t.Duration , + t.NTUserName , + t.NTDomainName , + t.HostName , + t.ApplicationName , + t.LoginName , + t.DBUserName + FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t + WHERE + ( + t.EventClass = 22 + AND t.Severity >= 17 + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + ) + OR + ( + t.EventClass IN (92, 93) + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + AND t.Duration > 15000000 + ) + OR + ( + t.EventClass IN (94, 95, 116) + ) + END; - Copyright (c) 2017 Brent Ozar Unlimited + SET @TraceFileIssue = 0 - 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: + END TRY + BEGIN CATCH - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + SET @TraceFileIssue = 1 + + END CATCH + + IF @TraceFileIssue = 1 + BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 199 ) + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + '199' AS CheckID , + '' AS DatabaseName , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'There Is An Error With The Default Trace' AS Finding , + 'https://www.brentozar.com/go/defaulttrace' AS URL , + 'Somebody has been messing with your trace files. Check the files are present at ' + @base_tracefilename AS Details + END + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 150 ) + AND @base_tracefilename IS NOT NULL + AND @TraceFileIssue = 0 + BEGIN - 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. + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 150) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 150 AS CheckID , + t.DatabaseName, + 170 AS Priority , + 'Reliability' AS FindingsGroup , + 'Errors Logged Recently in the Default Trace' AS Finding , + 'https://www.brentozar.com/go/defaulttrace' AS URL , + CAST(t.TextData AS NVARCHAR(4000)) AS Details + FROM #fnTraceGettable t + WHERE t.EventClass = 22 + /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ + --AND t.Severity >= 17 + --AND t.StartTime > DATEADD(dd, -30, GETDATE()); + END; + /* Performance - File Growths Slow */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 151 ) + AND @base_tracefilename IS NOT NULL + AND @TraceFileIssue = 0 + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 151) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 151 AS CheckID , + t.DatabaseName, + 50 AS Priority , + 'Performance' AS FindingsGroup , + 'File Growths Slow' AS Finding , + 'https://www.brentozar.com/go/filegrowth' AS URL , + CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting file autogrowth to a smaller increment.' AS Details + FROM #fnTraceGettable t + WHERE t.EventClass IN (92, 93) + /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ + --AND t.StartTime > DATEADD(dd, -30, GETDATE()) + --AND t.Duration > 15000000 + GROUP BY t.DatabaseName + HAVING COUNT(*) > 1; + END; + /* Performance - Many Plans for One Query */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 160 ) + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 160) WITH NOWAIT; + + SET @StringToExecute = N'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 160 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Many Plans for One Query'' AS Finding, + ''https://www.brentozar.com/go/parameterization'' AS URL, + CAST(COUNT(DISTINCT plan_handle) AS NVARCHAR(50)) + '' plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = ''dbid'' + GROUP BY qs.query_hash, pa.value + HAVING COUNT(DISTINCT plan_handle) > '; + IF 50 > (SELECT COUNT(*) FROM sys.databases) + SET @StringToExecute = @StringToExecute + N' 50 '; + ELSE + SELECT @StringToExecute = @StringToExecute + CAST(COUNT(*) * 2 AS NVARCHAR(50)) FROM sys.databases; - */'; - ELSE IF @OutputType = 'SCHEMA' - BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT'; + SET @StringToExecute = @StringToExecute + N' ORDER BY COUNT(DISTINCT plan_handle) DESC 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.'; + + EXECUTE(@StringToExecute); + END; - END; - ELSE /* IF @OutputType = 'SCHEMA' */ - BEGIN + /* Performance - High Number of Cached Plans */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 161 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 161) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 161 AS CheckID, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''High Number of Cached Plans'' AS Finding, + ''https://www.brentozar.com/go/planlimits'' AS URL, + ''Your server configuration is limited to '' + CAST(ht.buckets_count * 4 AS VARCHAR(20)) + '' '' + ht.name + '', and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details + FROM sys.dm_os_memory_cache_hash_tables ht + INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type + where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) + AND cc.entries_count >= (3 * ht.buckets_count) 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.'; + + EXECUTE(@StringToExecute); + END; - DECLARE @StringToExecute NVARCHAR(4000) - ,@curr_tracefilename NVARCHAR(500) - ,@base_tracefilename NVARCHAR(500) - ,@indx int - ,@query_result_separator CHAR(1) - ,@EmailSubject NVARCHAR(255) - ,@EmailBody NVARCHAR(MAX) - ,@EmailAttachmentFilename NVARCHAR(255) - ,@ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@CurrentName NVARCHAR(128) - ,@CurrentDefaultValue NVARCHAR(200) - ,@CurrentCheckID INT - ,@CurrentPriority INT - ,@CurrentFinding VARCHAR(200) - ,@CurrentURL VARCHAR(200) - ,@CurrentDetails NVARCHAR(4000) - ,@MsSinceWaitsCleared DECIMAL(38,0) - ,@CpuMsSinceWaitsCleared DECIMAL(38,0) - ,@ResultText NVARCHAR(MAX) - ,@crlf NVARCHAR(2) - ,@Processors int - ,@NUMANodes int - ,@MinServerMemory bigint - ,@MaxServerMemory bigint - ,@ColumnStoreIndexesInUse bit - ,@TraceFileIssue bit - -- Flag for Windows OS to help with Linux support - ,@IsWindowsOperatingSystem BIT; + /* Performance - Too Much Free Memory */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 165 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 165) WITH NOWAIT; + + INSERT INTO #BlitzResults + (CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://www.brentozar.com/go/freememory', + CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool, which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details + FROM sys.dm_os_performance_counters cFree + INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' + AND cTotal.counter_name = N'Total Server Memory (KB) ' + WHERE cFree.object_name LIKE N'%Memory Manager%' + AND cFree.counter_name = N'Free Memory (KB) ' + AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 + AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; + END; - SET @crlf = NCHAR(13) + NCHAR(10); - SET @ResultText = 'sp_Blitz Results: ' + @crlf; + /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 155 ) + AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 + BEGIN - /* - --TOURSTOP01-- - See https://www.BrentOzar.com/go/blitztour for a guided tour. - - We start by creating #BlitzResults. It's a temp table that will store all of - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 155) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 155 AS CheckID , + 0 AS Priority , + 'Outdated sp_Blitz' AS FindingsGroup , + 'sp_Blitz is Over 6 Months Old' AS Finding , + 'http://FirstResponderKit.org/' AS URL , + 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details; + END; - #BlitzResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - For a list of checks, visit http://FirstResponderKit.org. - */ - IF OBJECT_ID('tempdb..#BlitzResults') IS NOT NULL - DROP TABLE #BlitzResults; - CREATE TABLE #BlitzResults - ( - ID INT IDENTITY(1, 1) , - CheckID INT , - DatabaseName NVARCHAR(128) , - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL - ); + /* Populate a list of database defaults. I'm doing this kind of oddly - + it reads like a lot of work, but this way it compiles & runs on all + versions of SQL Server. + */ + + IF @Debug IN (1, 2) RAISERROR('Generating database defaults.', 0, 1) WITH NOWAIT; + + INSERT INTO #DatabaseDefaults + SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_read_committed_snapshot_on', + CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END, /* RCSI is always enabled in Azure SQL DB per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + 133, 210, CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 'Read Committed Snapshot Isolation Disabled' ELSE 'Read Committed Snapshot Isolation Enabled' END, 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_broker_enabled', 0, 230, 210, 'Broker Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_broker_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_honor_broker_priority_on', 0, 231, 210, 'Honor Broker Priority Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_honor_broker_priority_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); + /* Not alerting for this since we actually want it and we have a separate check for it: + INSERT INTO #DatabaseDefaults + SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); + */ + INSERT INTO #DatabaseDefaults + SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); + --INSERT INTO #DatabaseDefaults + -- SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://www.brentozar.com/go/dbdefaults', NULL + -- FROM sys.all_columns + -- WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases') + AND SERVERPROPERTY('EngineEdition') <> 8; /* Hekaton is always enabled in Managed Instances per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #DatabaseDefaults + SELECT 'is_accelerated_database_recovery_on', 0, 145, 210, 'Acclerated Database Recovery Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_accelerated_database_recovery_on' AND object_id = OBJECT_ID('sys.databases') AND SERVERPROPERTY('EngineEdition') NOT IN (5, 8) ; - IF OBJECT_ID('tempdb..#TemporaryDatabaseResults') IS NOT NULL - DROP TABLE #TemporaryDatabaseResults; - CREATE TABLE #TemporaryDatabaseResults - ( - DatabaseName NVARCHAR(128) , - Finding NVARCHAR(128) - ); + DECLARE DatabaseDefaultsLoop CURSOR FOR + SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details + FROM #DatabaseDefaults; - /* - You can build your own table with a list of checks to skip. For example, you - might have some databases that you don't care about, or some checks you don't - want to run. Then, when you run sp_Blitz, you can specify these parameters: - @SkipChecksDatabase = 'DBAtools', - @SkipChecksSchema = 'dbo', - @SkipChecksTable = 'BlitzChecksToSkip' - Pass in the database, schema, and table that contains the list of checks you - want to skip. This part of the code checks those parameters, gets the list, - and then saves those in a temp table. As we run each check, we'll see if we - need to skip it. + OPEN DatabaseDefaultsLoop; + FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; + WHILE @@FETCH_STATUS = 0 + BEGIN - Really anal-retentive users will note that the @SkipChecksServer parameter is - not used. YET. We added that parameter in so that we could avoid changing the - stored proc's surface area (interface) later. - */ - /* --TOURSTOP07-- */ - IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL - DROP TABLE #SkipChecks; - CREATE TABLE #SkipChecks - ( - DatabaseName NVARCHAR(128) , - CheckID INT , - ServerName NVARCHAR(128) - ); - CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, @CurrentCheckID) WITH NOWAIT; - IF @SkipChecksTable IS NOT NULL - AND @SkipChecksSchema IS NOT NULL - AND @SkipChecksDatabase IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Inserting SkipChecks', 0, 1) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) - SELECT DISTINCT DatabaseName, CheckID, ServerName - FROM ' + QUOTENAME(@SkipChecksDatabase) + '.' + QUOTENAME(@SkipChecksSchema) + '.' + QUOTENAME(@SkipChecksTable) - + ' WHERE ServerName IS NULL OR ServerName = SERVERPROPERTY(''ServerName'') OPTION (RECOMPILE);'; - EXEC(@StringToExecute); - END; + /* Target Recovery Time (142) can be either 0 or 60 due to a number of bugs */ + IF @CurrentCheckID = 142 + 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);'; + 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);'; + + 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.'; + + EXEC (@StringToExecute); - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - BEGIN - -- Flag for Windows OS to help with Linux support - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) + FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; + END; + + CLOSE DatabaseDefaultsLoop; + DEALLOCATE DatabaseDefaultsLoop; + +/* Check if target recovery interval <> 60 */ +IF + @ProductVersionMajor >= 10 + AND NOT EXISTS + ( + SELECT + 1/0 + FROM #SkipChecks AS sc + WHERE sc.DatabaseName IS NULL + AND sc.CheckID = 257 + ) + BEGIN + IF EXISTS + ( + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.name = 'target_recovery_time_in_seconds' + AND ac.object_id = OBJECT_ID('sys.databases') + ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 257) WITH NOWAIT; + + DECLARE + @tri nvarchar(max) = N' + SELECT + DatabaseName = + d.name, + CheckId = + 257, + Priority = + 50, + FindingsGroup = + N''Performance'', + Finding = + N''Recovery Interval Not Optimal'', + URL = + N''https://sqlperformance.com/2020/05/system-configuration/0-to-60-switching-to-indirect-checkpoints'', + Details = + N''The database '' + + QUOTENAME(d.name) + + N'' has a target recovery interval of '' + + RTRIM(d.target_recovery_time_in_seconds) + + CASE + WHEN d.target_recovery_time_in_seconds = 0 + THEN N'', which is a legacy default, and should be changed to 60.'' + WHEN d.target_recovery_time_in_seconds <> 0 + THEN N'', which is probably a mistake, and should be changed to 60.'' + END + FROM sys.databases AS d + WHERE d.database_id > 4 + AND d.is_read_only = 0 + AND d.is_in_standby = 0 + AND d.target_recovery_time_in_seconds <> 60; + '; + + INSERT INTO + #BlitzResults + ( + DatabaseName, + CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details + ) + EXEC sys.sp_executesql + @tri; + + END; + END; + + +/*This checks to see if Agent is Offline*/ +IF @ProductVersionMajor >= 10 + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 167 ) BEGIN - SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 167) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 167 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Agent is Currently Offline' AS [Finding] , + '' AS [URL] , + ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' + ) AS [Details] + FROM + [sys].[dm_server_services] + WHERE [status_desc] <> 'Running' + AND [servicename] LIKE 'SQL Server Agent%' + AND CAST(SERVERPROPERTY('Edition') AS VARCHAR(1000)) NOT LIKE '%xpress%'; + END; - ELSE + END; +/* CheckID 258 - Security - SQL Server Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 258 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE [name] = 'dm_server_services' ) BEGIN - SELECT @IsWindowsOperatingSystem = 1 ; - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 258) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; - set @curr_tracefilename = reverse(@curr_tracefilename); + SELECT + 258 AS [CheckID] , + 1 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Dangerous Service Account' AS [Finding] , + 'https://vladdba.com/SQLServerSvcAccount' AS [URL] , + 'SQL Server''s service account is '+ [service_account] + +' - meaning that anyone who can use xp_cmdshell can do absolutely anything on the host.' AS [Details] + FROM + [sys].[dm_server_services] + WHERE ([service_account] = 'LocalSystem' + OR LOWER([service_account]) = 'nt authority\system') + AND [servicename] LIKE 'SQL Server%' + AND [servicename] NOT LIKE 'SQL Server Agent%'; + END; + END; - -- Set the trace file path separator based on underlying OS - IF (@IsWindowsOperatingSystem = 1) +/* CheckID 259 - Security - SQL Server Agent Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 259 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE [name] = 'dm_server_services' ) BEGIN - select @indx = patindex('%\%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '\log.trc' ; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 259) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 259 AS [CheckID] , + 1 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Dangerous Service Account' AS [Finding] , + 'https://vladdba.com/SQLServerSvcAccount' AS [URL] , + 'SQL Server Agent''s service account is '+ [service_account] + +' - meaning that anyone who can create and run jobs can do absolutely anything on the host.' AS [Details] + FROM + [sys].[dm_server_services] + WHERE ([service_account] = 'LocalSystem' + OR LOWER([service_account]) = 'nt authority\system') + AND [servicename] LIKE 'SQL Server Agent%'; END; - ELSE + END; + +/*This checks to see if the Full Text thingy is offline*/ +IF @ProductVersionMajor >= 10 + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 168 ) BEGIN - select @indx = patindex('%/%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '/log.trc' ; + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 168 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , + '' AS [URL] , + ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' + ) AS [Details] + FROM + [sys].[dm_server_services] + WHERE [status_desc] <> 'Running' + AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; + + END; END; - END; +/*This checks which service account SQL Server is running as.*/ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 169 ) - /* If the server has any databases on Antiques Roadshow, skip the checks that would break due to CTEs. */ - IF @CheckUserDatabaseObjects = 1 AND EXISTS(SELECT * FROM sys.databases WHERE compatibility_level < 90) - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Databases with compatibility level < 90 found, so setting @CheckUserDatabaseObjects = 0.'; - PRINT 'The database-level checks rely on CTEs, which are not supported in SQL 2000 compat level databases.'; - PRINT 'Get with the cool kids and switch to a current compatibility level, Grandpa. To find the problems, run:'; - PRINT 'SELECT * FROM sys.databases WHERE compatibility_level < 90;'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 204 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'Since you have databases with compatibility_level < 90, we can''t run @CheckUserDatabaseObjects = 1. To find them: SELECT * FROM sys.databases WHERE compatibility_level < 90' AS Details; - END; + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 169) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT + 169 AS [CheckID] , + 250 AS [Priority] , + 'Informational' AS [FindingsGroup] , + 'SQL Server is running under an NT Service account' AS [Finding] , + 'https://www.brentozar.com/go/setup' AS [URL] , + ( 'I''m running as ' + [service_account] + '.' + ) AS [Details] + FROM + [sys].[dm_server_services] + WHERE [service_account] LIKE 'NT Service%' + AND [servicename] LIKE 'SQL Server%' + AND [servicename] NOT LIKE 'SQL Server Agent%' + AND [servicename] NOT LIKE 'SQL Server Launchpad%'; - /* --TOURSTOP08-- */ - /* If the server is Amazon RDS, skip checks that it doesn't allow */ - IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('ServerName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (6); - INSERT INTO #SkipChecks (CheckID) VALUES (29); - INSERT INTO #SkipChecks (CheckID) VALUES (30); - INSERT INTO #SkipChecks (CheckID) VALUES (31); - INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file */ - INSERT INTO #SkipChecks (CheckID) VALUES (57); - INSERT INTO #SkipChecks (CheckID) VALUES (59); - INSERT INTO #SkipChecks (CheckID) VALUES (61); - INSERT INTO #SkipChecks (CheckID) VALUES (62); - INSERT INTO #SkipChecks (CheckID) VALUES (68); - INSERT INTO #SkipChecks (CheckID) VALUES (69); - INSERT INTO #SkipChecks (CheckID) VALUES (73); - INSERT INTO #SkipChecks (CheckID) VALUES (79); - INSERT INTO #SkipChecks (CheckID) VALUES (92); - INSERT INTO #SkipChecks (CheckID) VALUES (94); - INSERT INTO #SkipChecks (CheckID) VALUES (96); - INSERT INTO #SkipChecks (CheckID) VALUES (98); - INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled */ - INSERT INTO #SkipChecks (CheckID) VALUES (123); - INSERT INTO #SkipChecks (CheckID) VALUES (177); - INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans */ - INSERT INTO #SkipChecks (CheckID) VALUES (181); - END; /* Amazon RDS skipped checks */ + END; + END; - /* If the server is ExpressEdition, skip checks that it doesn't allow */ - IF CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (30); /* Alerts not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (31); /* Operators not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (61); /* Agent alerts 19-25 */ - INSERT INTO #SkipChecks (CheckID) VALUES (73); /* Failsafe operator */ - INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ - END; /* Express Edition skipped checks */ +/*This checks which service account SQL Agent is running as.*/ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 170 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 170) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - /* - That's the end of the SkipChecks stuff. - The next several tables are used by various checks later. - */ - IF OBJECT_ID('tempdb..#ConfigurationDefaults') IS NOT NULL - DROP TABLE #ConfigurationDefaults; - CREATE TABLE #ConfigurationDefaults - ( - name NVARCHAR(128) , - DefaultValue BIGINT, - CheckID INT - ); + SELECT + 170 AS [CheckID] , + 250 AS [Priority] , + 'Informational' AS [FindingsGroup] , + 'SQL Server Agent is running under an NT Service account' AS [Finding] , + 'https://www.brentozar.com/go/setup' AS [URL] , + ( 'I''m running as ' + [service_account] + '.' + ) AS [Details] + FROM + [sys].[dm_server_services] + WHERE [service_account] LIKE 'NT Service%' + AND [servicename] LIKE 'SQL Server Agent%'; - IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL - DROP TABLE #Recompile; - CREATE TABLE #Recompile( - DBName varchar(200), - ProcName varchar(300), - RecompileFlag varchar(1), - SPSchema varchar(50) - ); + END; + END; + +/*This checks that First Responder Kit is consistent. +It assumes that all the objects of the kit resides in the same database, the one in which this SP is stored +It also is ready to check for installation in another schema. +*/ +IF( + NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 226 + ) +) +BEGIN - IF OBJECT_ID('tempdb..#DatabaseDefaults') IS NOT NULL - DROP TABLE #DatabaseDefaults; - CREATE TABLE #DatabaseDefaults - ( - name NVARCHAR(128) , - DefaultValue NVARCHAR(200), - CheckID INT, - Priority INT, - Finding VARCHAR(200), - URL VARCHAR(200), - Details NVARCHAR(4000) - ); + IF @Debug IN (1, 2) RAISERROR('Running check with id %d',0,1,2000); + + SET @spBlitzFullName = QUOTENAME(DB_NAME()) + '.' +QUOTENAME(OBJECT_SCHEMA_NAME(@@PROCID)) + '.' + QUOTENAME(OBJECT_NAME(@@PROCID)); + SET @BlitzIsOutdatedComparedToOthers = 0; + SET @tsql = NULL; + SET @VersionCheckModeExistsTSQL = NULL; + SET @BlitzProcDbName = DB_NAME(); + SET @ExecRet = NULL; + SET @InnerExecRet = NULL; + SET @TmpCnt = NULL; + + SET @PreviousComponentName = NULL; + SET @PreviousComponentFullPath = NULL; + SET @CurrentStatementId = NULL; + SET @CurrentComponentSchema = NULL; + SET @CurrentComponentName = NULL; + SET @CurrentComponentType = NULL; + SET @CurrentComponentVersionDate = NULL; + SET @CurrentComponentFullName = NULL; + SET @CurrentComponentMandatory = NULL; + SET @MaximumVersionDate = NULL; + + SET @StatementCheckName = NULL; + SET @StatementOutputsCounter = NULL; + SET @OutputCounterExpectedValue = NULL; + SET @StatementOutputsExecRet = NULL; + SET @StatementOutputsDateTime = NULL; + + SET @CurrentComponentMandatoryCheckOK = NULL; + SET @CurrentComponentVersionCheckModeOK = NULL; + + SET @canExitLoop = 0; + SET @frkIsConsistent = 0; + + + SET @tsql = 'USE ' + QUOTENAME(@BlitzProcDbName) + ';' + @crlf + + 'WITH FRKComponents (' + @crlf + + ' ObjectName,' + @crlf + + ' ObjectType,' + @crlf + + ' MandatoryComponent' + @crlf + + ')' + @crlf + + 'AS (' + @crlf + + ' SELECT ''sp_AllNightLog'',''P'' ,0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''sp_AllNightLog_Setup'', ''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_Blitz'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzBackups'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzCache'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzFirst'',''P'',0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''sp_BlitzIndex'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzLock'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzQueryStore'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzWho'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_DatabaseRestore'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_ineachdb'',''P'',0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''SqlServerVersions'',''U'',0' + @crlf + + ')' + @crlf + + 'INSERT INTO #FRKObjects (' + @crlf + + ' DatabaseName,ObjectSchemaName,ObjectName, ObjectType,MandatoryComponent' + @crlf + + ')' + @crlf + + 'SELECT DB_NAME(),SCHEMA_NAME(o.schema_id), c.ObjectName,c.ObjectType,c.MandatoryComponent' + @crlf + + 'FROM ' + @crlf + + ' FRKComponents c' + @crlf + + 'LEFT JOIN ' + @crlf + + ' sys.objects o' + @crlf + + 'ON c.ObjectName = o.[name]' + @crlf + + 'AND c.ObjectType = o.[type]' + @crlf + + --'WHERE o.schema_id IS NOT NULL' + @crlf + + ';' + ; + + EXEC @ExecRet = sp_executesql @tsql ; + + -- TODO: add check for statement success + + -- TODO: based on SP requirements and presence (SchemaName is not null) ==> update MandatoryComponent column + + -- Filling #StatementsToRun4FRKVersionCheck + INSERT INTO #StatementsToRun4FRKVersionCheck ( + CheckName,StatementText,SubjectName,SubjectFullPath, StatementOutputsCounter,OutputCounterExpectedValue,StatementOutputsExecRet,StatementOutputsDateTime + ) + SELECT + 'Mandatory', + 'SELECT @cnt = COUNT(*) FROM #FRKObjects WHERE ObjectSchemaName IS NULL AND ObjectName = ''' + ObjectName + ''' AND MandatoryComponent = 1;', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 1, + 0, + 0, + 0 + FROM #FRKObjects + UNION ALL + SELECT + 'VersionCheckMode', + 'SELECT @cnt = COUNT(*) FROM ' + + QUOTENAME(DatabaseName) + '.sys.all_parameters ' + + 'where object_id = OBJECT_ID(''' + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName) + ''') AND [name] = ''@VersionCheckMode'';', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 1, + 1, + 0, + 0 + FROM #FRKObjects + WHERE ObjectType = 'P' + AND ObjectSchemaName IS NOT NULL + UNION ALL + SELECT + 'VersionCheck', + 'EXEC @ExecRet = ' + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName) + ' @VersionCheckMode = 1 , @VersionDate = @ObjDate OUTPUT;', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 0, + 0, + 1, + 1 + FROM #FRKObjects + WHERE ObjectType = 'P' + AND ObjectSchemaName IS NOT NULL + ; + IF(@Debug in (1,2)) + BEGIN + SELECT * + FROM #StatementsToRun4FRKVersionCheck ORDER BY SubjectName,SubjectFullPath,StatementId -- in case of schema change ; + END; + + + -- loop on queries... + WHILE(@canExitLoop = 0) + BEGIN + SET @CurrentStatementId = NULL; + + SELECT TOP 1 + @StatementCheckName = CheckName, + @CurrentStatementId = StatementId , + @CurrentComponentName = SubjectName, + @CurrentComponentFullName = SubjectFullPath, + @tsql = StatementText, + @StatementOutputsCounter = StatementOutputsCounter, + @OutputCounterExpectedValue = OutputCounterExpectedValue , + @StatementOutputsExecRet = StatementOutputsExecRet, + @StatementOutputsDateTime = StatementOutputsDateTime + FROM #StatementsToRun4FRKVersionCheck + ORDER BY SubjectName, SubjectFullPath,StatementId /* in case of schema change */ + ; + + -- loop exit condition + IF(@CurrentStatementId IS NULL) + BEGIN + BREAK; + END; - IF OBJECT_ID('tempdb..#DatabaseScopedConfigurationDefaults') IS NOT NULL - DROP TABLE #DatabaseScopedConfigurationDefaults; - CREATE TABLE #DatabaseScopedConfigurationDefaults - (ID INT IDENTITY(1,1), configuration_id INT, [name] NVARCHAR(60), default_value sql_variant, default_value_for_secondary sql_variant, CheckID INT, ); + IF @Debug IN (1, 2) RAISERROR(' Statement: %s',0,1,@tsql); + -- we start a new component + IF(@PreviousComponentName IS NULL OR + (@PreviousComponentName IS NOT NULL AND @PreviousComponentName <> @CurrentComponentName) OR + (@PreviousComponentName IS NOT NULL AND @PreviousComponentName = @CurrentComponentName AND @PreviousComponentFullPath <> @CurrentComponentFullName) + ) + BEGIN + -- reset variables + SET @CurrentComponentMandatoryCheckOK = 0; + SET @CurrentComponentVersionCheckModeOK = 0; + SET @PreviousComponentName = @CurrentComponentName; + SET @PreviousComponentFullPath = @CurrentComponentFullName ; + END; + IF(@StatementCheckName NOT IN ('Mandatory','VersionCheckMode','VersionCheck')) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (code generator changed)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Your version check failed because a change has been made to the version check code generator.' + @crlf + + 'Error: No handler for check with name "' + ISNULL(@StatementCheckName,'') + '"' AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL - DROP TABLE #DBCCs; - CREATE TABLE #DBCCs - ( - ID INT IDENTITY(1, 1) - PRIMARY KEY , - ParentObject VARCHAR(255) , - Object VARCHAR(255) , - Field VARCHAR(255) , - Value VARCHAR(255) , - DbName NVARCHAR(128) NULL - ); + IF(@StatementCheckName = 'Mandatory') + BEGIN + -- outputs counter + EXEC @ExecRet = sp_executesql @tsql, N'@cnt INT OUTPUT',@cnt = @TmpCnt OUTPUT; + IF(@ExecRet <> 0) + BEGIN + + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Your version check failed due to dynamic query failure.' + @crlf + + 'Error: following query failed at execution (check if component [' + ISNULL(@CurrentComponentName,@CurrentComponentName) + '] is mandatory and missing)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL - DROP TABLE #LogInfo2012; - CREATE TABLE #LogInfo2012 - ( - recoveryunitid INT , - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); + IF(@TmpCnt <> @OutputCounterExpectedValue) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 227 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Missing: ' + @CurrentComponentName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated version of the First Responder Kit to install it.' AS Details + ; + + -- as it's missing, no value for SubjectFullPath + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectName = @CurrentComponentName ; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#LogInfo') IS NOT NULL - DROP TABLE #LogInfo; - CREATE TABLE #LogInfo - ( - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); + SET @CurrentComponentMandatoryCheckOK = 1; + END; + + IF(@StatementCheckName = 'VersionCheckMode') + BEGIN + IF(@CurrentComponentMandatoryCheckOK = 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (unexpectedly modified checks ordering)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because "Mandatory" check has not been completed before for current component' + @crlf + + 'Error: version check mode happenned before "Mandatory" check for component called "' + @CurrentComponentFullName + '"' + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#partdb') IS NOT NULL - DROP TABLE #partdb; - CREATE TABLE #partdb - ( - dbname NVARCHAR(128) , - objectname NVARCHAR(200) , - type_desc NVARCHAR(128) - ); + -- outputs counter + EXEC @ExecRet = sp_executesql @tsql, N'@cnt INT OUTPUT',@cnt = @TmpCnt OUTPUT; - IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); + IF(@ExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because a change has been made to the code generator.' + @crlf + + 'Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] can run in VersionCheckMode)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#driveInfo') IS NOT NULL - DROP TABLE #driveInfo; - CREATE TABLE #driveInfo - ( - drive NVARCHAR , - SIZE DECIMAL(18, 2) - ); + IF(@TmpCnt <> @OutputCounterExpectedValue) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 228 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Outdated: ' + @CurrentComponentFullName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Component ' + @CurrentComponentFullName + ' is not at the minimum version required to run this procedure' + @crlf + + 'VersionCheckMode has been introduced in component version date after "20190320". This means its version is lower than or equal to that date.' AS Details; + ; + + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; + SET @CurrentComponentVersionCheckModeOK = 1; + END; - IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL - DROP TABLE #dm_exec_query_stats; - CREATE TABLE #dm_exec_query_stats - ( - [id] [int] NOT NULL - IDENTITY(1, 1) , - [sql_handle] [varbinary](64) NOT NULL , - [statement_start_offset] [int] NOT NULL , - [statement_end_offset] [int] NOT NULL , - [plan_generation_num] [bigint] NOT NULL , - [plan_handle] [varbinary](64) NOT NULL , - [creation_time] [datetime] NOT NULL , - [last_execution_time] [datetime] NOT NULL , - [execution_count] [bigint] NOT NULL , - [total_worker_time] [bigint] NOT NULL , - [last_worker_time] [bigint] NOT NULL , - [min_worker_time] [bigint] NOT NULL , - [max_worker_time] [bigint] NOT NULL , - [total_physical_reads] [bigint] NOT NULL , - [last_physical_reads] [bigint] NOT NULL , - [min_physical_reads] [bigint] NOT NULL , - [max_physical_reads] [bigint] NOT NULL , - [total_logical_writes] [bigint] NOT NULL , - [last_logical_writes] [bigint] NOT NULL , - [min_logical_writes] [bigint] NOT NULL , - [max_logical_writes] [bigint] NOT NULL , - [total_logical_reads] [bigint] NOT NULL , - [last_logical_reads] [bigint] NOT NULL , - [min_logical_reads] [bigint] NOT NULL , - [max_logical_reads] [bigint] NOT NULL , - [total_clr_time] [bigint] NOT NULL , - [last_clr_time] [bigint] NOT NULL , - [min_clr_time] [bigint] NOT NULL , - [max_clr_time] [bigint] NOT NULL , - [total_elapsed_time] [bigint] NOT NULL , - [last_elapsed_time] [bigint] NOT NULL , - [min_elapsed_time] [bigint] NOT NULL , - [max_elapsed_time] [bigint] NOT NULL , - [query_hash] [binary](8) NULL , - [query_plan_hash] [binary](8) NULL , - [query_plan] [xml] NULL , - [query_plan_filtered] [nvarchar](MAX) NULL , - [text] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL , - [text_filtered] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL - ); - - IF OBJECT_ID('tempdb..#ErrorLog') IS NOT NULL - DROP TABLE #ErrorLog; - CREATE TABLE #ErrorLog - ( - LogDate DATETIME , - ProcessInfo NVARCHAR(20) , - [Text] NVARCHAR(1000) - ); - - IF OBJECT_ID('tempdb..#fnTraceGettable') IS NOT NULL - DROP TABLE #fnTraceGettable; - CREATE TABLE #fnTraceGettable - ( - TextData NVARCHAR(4000) , - DatabaseName NVARCHAR(256) , - EventClass INT , - Severity INT , - StartTime DATETIME , - EndTime DATETIME , - Duration BIGINT , - NTUserName NVARCHAR(256) , - NTDomainName NVARCHAR(256) , - HostName NVARCHAR(256) , - ApplicationName NVARCHAR(256) , - LoginName NVARCHAR(256) , - DBUserName NVARCHAR(256) - ); + IF(@StatementCheckName = 'VersionCheck') + BEGIN + IF(@CurrentComponentMandatoryCheckOK = 0 OR @CurrentComponentVersionCheckModeOK = 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (unexpectedly modified checks ordering)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because "VersionCheckMode" check has not been completed before for component called "' + @CurrentComponentFullName + '"' + @crlf + + 'Error: VersionCheck happenned before "VersionCheckMode" check for component called "' + @CurrentComponentFullName + '"' + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; - IF OBJECT_ID('tempdb..#IgnorableWaits') IS NOT NULL - DROP TABLE #IgnorableWaits; - CREATE TABLE #IgnorableWaits (wait_type NVARCHAR(60)); - INSERT INTO #IgnorableWaits VALUES ('BROKER_EVENTHANDLER'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_RECEIVE_WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TASK_STOP'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TO_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TRANSMITTER'); - INSERT INTO #IgnorableWaits VALUES ('CHECKPOINT_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('CLR_AUTO_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_MANUAL_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_EVENTS_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_WORKER_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRRORING_CMD'); - INSERT INTO #IgnorableWaits VALUES ('DIRTY_PAGE_POLL'); - INSERT INTO #IgnorableWaits VALUES ('DISPATCHER_QUEUE_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTSHC_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('HADR_CLUSAPI_CALL'); - INSERT INTO #IgnorableWaits VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION'); - INSERT INTO #IgnorableWaits VALUES ('HADR_LOGCAPTURE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('HADR_NOTIFICATION_DEQUEUE'); - INSERT INTO #IgnorableWaits VALUES ('HADR_TIMER_TASK'); - INSERT INTO #IgnorableWaits VALUES ('HADR_WORK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('LAZYWRITER_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('LOGMGR_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('ONDEMAND_TASK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS'); - INSERT INTO #IgnorableWaits VALUES ('QDS_ASYNC_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_SHUTDOWN_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('REDO_THREAD_PENDING_WORK'); - INSERT INTO #IgnorableWaits VALUES ('REQUEST_FOR_DEADLOCK_SEARCH'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_SYSTEMTASK'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_TASK'); - INSERT INTO #IgnorableWaits VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_BUFFER_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('UCS_SESSION_REGISTRATION'); - INSERT INTO #IgnorableWaits VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'); - INSERT INTO #IgnorableWaits VALUES ('WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('XE_DISPATCHER_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('XE_LIVE_TARGET_TVF'); - INSERT INTO #IgnorableWaits VALUES ('XE_TIMER_EVENT'); - - IF @Debug IN (1, 2) RAISERROR('Setting @MsSinceWaitsCleared', 0, 1) WITH NOWAIT; + EXEC @ExecRet = sp_executesql @tsql , N'@ExecRet INT OUTPUT, @ObjDate DATETIME OUTPUT', @ExecRet = @InnerExecRet OUTPUT, @ObjDate = @CurrentComponentVersionDate OUTPUT; - SELECT @MsSinceWaitsCleared = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) * 60000.0 - FROM sys.databases - WHERE name = 'tempdb'; + IF(@ExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. The version check failed because a change has been made to the code generator.' + @crlf + + 'Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] is at the expected version)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + + IF(@InnerExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (Failed dynamic SP call to ' + @CurrentComponentFullName + ')' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] is at the expected version)' + @crlf + + 'Return code: ' + CONVERT(VARCHAR(10),@InnerExecRet) + @crlf + + 'T-SQL Query: ' + @crlf + + @tsql AS Details + ; + + -- advance to next component + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; - /* Have they cleared wait stats? Using a 10% fudge factor */ - IF @MsSinceWaitsCleared * .9 > (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 185) WITH NOWAIT; + IF(@CurrentComponentVersionDate < @VersionDate) + BEGIN + + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 228 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Outdated: ' + @CurrentComponentFullName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download and install the latest First Responder Kit - you''re running some older code, and it doesn''t get better with age.' AS Details + ; + + RAISERROR('Component %s is outdated',10,1,@CurrentComponentFullName); + -- advance to next component + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; - SET @MsSinceWaitsCleared = (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')); - IF @MsSinceWaitsCleared = 0 SET @MsSinceWaitsCleared = 1; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES( 185, - 240, - 'Wait Stats', - 'Wait Stats Have Been Cleared', - 'https://BrentOzar.com/go/waits', - 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + CONVERT(NVARCHAR(100), DATEADD(ms, (-1 * @MsSinceWaitsCleared), GETDATE()), 120)); - END; + ELSE IF(@CurrentComponentVersionDate > @VersionDate AND @BlitzIsOutdatedComparedToOthers = 0) + BEGIN + SET @BlitzIsOutdatedComparedToOthers = 1; + RAISERROR('Procedure %s is outdated',10,1,@spBlitzFullName); + IF(@MaximumVersionDate IS NULL OR @MaximumVersionDate < @CurrentComponentVersionDate) + BEGIN + SET @MaximumVersionDate = @CurrentComponentVersionDate; + END; + END; + /* Kept for debug purpose: + ELSE + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 2000 AS CheckID , + 250 AS Priority , + 'Informational' AS FindingsGroup , + 'First Responder kit component ' + @CurrentComponentFullName + ' is at the expected version' AS Finding , + 'https://www.BrentOzar.com/blitz/' AS URL , + 'Version date is: ' + CONVERT(VARCHAR(32),@CurrentComponentVersionDate,121) AS Details + ; + END; + */ + END; - /* @CpuMsSinceWaitsCleared is used for waits stats calculations */ - - IF @Debug IN (1, 2) RAISERROR('Setting @CpuMsSinceWaitsCleared', 0, 1) WITH NOWAIT; - - SELECT @CpuMsSinceWaitsCleared = @MsSinceWaitsCleared * scheduler_count - FROM sys.dm_os_sys_info; + -- could be performed differently to minimize computation + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE StatementId = @CurrentStatementId ; + END; +END; - /* If we're outputting CSV or Markdown, don't bother checking the plan cache because we cannot export plans. */ - IF @OutputType = 'CSV' OR @OutputType = 'MARKDOWN' - SET @CheckProcedureCache = 0; +/*This counts memory dumps and gives min and max date of in view*/ +IF @ProductVersionMajor >= 10 + AND NOT (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 4297) /* Skip due to crash bug: https://support.microsoft.com/en-us/help/2908087 */ + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 171 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_memory_dumps' ) + BEGIN + IF EXISTS (SELECT * FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) - /* If we're posting a question on Stack, include background info on the server */ - IF @OutputType = 'MARKDOWN' - SET @CheckServerInfo = 1; + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 171) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT + 171 AS [CheckID] , + 20 AS [Priority] , + 'Reliability' AS [FindingsGroup] , + 'Memory Dumps Have Occurred' AS [Finding] , + 'https://www.brentozar.com/go/dump' AS [URL] , + ( 'That ain''t good. I''ve had ' + + CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + + CAST(CAST(MIN([creation_time]) AS DATETIME) AS VARCHAR(100)) + + ' and ' + + CAST(CAST(MAX([creation_time]) AS DATETIME) AS VARCHAR(100)) + + '!' + ) AS [Details] + FROM + [sys].[dm_server_memory_dumps] + WHERE [creation_time] >= DATEADD(year, -1, GETDATE()); - /* Only run CheckUserDatabaseObjects if there are less than 50 databases. */ - IF @BringThePain = 0 AND 50 <= (SELECT COUNT(*) FROM sys.databases) AND @CheckUserDatabaseObjects = 1 - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary insanity for the server and/or user.'; - PRINT 'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 201 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'If you want to check 50+ databases, you have to also use @BringThePain = 1.' AS Details; - END; + END; + END; + END; - /* Sanitize our inputs */ - SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); +/*Checks to see if you're on Developer or Evaluation*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 173 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 173) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - /* Get the major and minor build numbers */ - - IF @Debug IN (1, 2) RAISERROR('Getting version information.', 0, 1) WITH NOWAIT; - - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(varchar(32), @ProductVersion), 2); - - /* - Whew! we're finally done with the setup, and we can start doing checks. - First, let's make sure we're actually supposed to do checks on this server. - The user could have passed in a SkipChecks table that specified to skip ALL - checks on this server, so let's check for that: - */ - IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID IS NULL ) ) - OR ( @SkipChecksTable IS NULL ) - ) - BEGIN + SELECT + 173 AS [CheckID] , + 200 AS [Priority] , + 'Licensing' AS [FindingsGroup] , + 'Non-Production License' AS [Finding] , + 'https://www.brentozar.com/go/licensing' AS [URL] , + ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + + ' the good folks at Microsoft might get upset with you. Better start counting those cores.' + ) AS [Details] + WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Developer%' + OR CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Evaluation%'; - /* - Our very first check! We'll put more comments in this one just to - explain exactly how it works. First, we check to see if we're - supposed to skip CheckID 1 (that's the check we're working on.) - */ - IF NOT EXISTS ( SELECT 1 + END; + +/*Checks to see if Buffer Pool Extensions are in use*/ + IF @ProductVersionMajor >= 12 + AND NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 1 ) + WHERE DatabaseName IS NULL AND CheckID = 174 ) BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 174) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - /* - Below, we check master.sys.databases looking for databases - that haven't had a backup in the last week. If we find any, - we insert them into #BlitzResults, the temp table that - tracks our server's problems. Note that if the check does - NOT find any problems, we don't save that. We're only - saving the problems, not the successful checks. - */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 1) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 1 AS CheckID , - d.[name] AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backups Not Performed Recently' AS Finding , - 'https://BrentOzar.com/go/nobak' AS URL , - 'Last backed up: ' - + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details - FROM master.sys.databases d - LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'D' - AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ - WHERE d.database_id <> 2 /* Bonus points if you know what that means */ - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 1) - /* - The above NOT IN filters out the databases we're not supposed to check. - */ - GROUP BY d.name - HAVING MAX(b.backup_finish_date) <= DATEADD(dd, - -7, GETDATE()) - OR MAX(b.backup_finish_date) IS NULL; - /* - And there you have it. The rest of this stored procedure works the same - way: it asks: - - Should I skip this check? - - If not, do I find problems? - - Insert the results into #BlitzResults - */ + SELECT + 174 AS [CheckID] , + 200 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Buffer Pool Extensions Enabled' AS [Finding] , + 'https://www.brentozar.com/go/bpe' AS [URL] , + ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + + [path] + + '. It''s currently ' + + CASE WHEN [current_size_in_kb] / 1024. / 1024. > 0 + THEN CAST([current_size_in_kb] / 1024. / 1024. AS VARCHAR(100)) + + ' GB' + ELSE CAST([current_size_in_kb] / 1024. AS VARCHAR(100)) + + ' MB' + END + + '. Did you know that BPEs only provide single threaded access 8KB (one page) at a time?' + ) AS [Details] + FROM sys.dm_os_buffer_pool_extension_configuration + WHERE [state_description] <> 'BUFFER POOL EXTENSION DISABLED'; END; - /* - And that's the end of CheckID #1. - - CheckID #2 is a little simpler because it only involves one query, and it's - more typical for queries that people contribute. But keep reading, because - the next check gets more complex again. - */ - - IF NOT EXISTS ( SELECT 1 +/*Check for too many tempdb files*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 2 ) + WHERE DatabaseName IS NULL AND CheckID = 175 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 175) WITH NOWAIT; + INSERT INTO #BlitzResults ( CheckID , DatabaseName , @@ -3558,1492 +5839,1479 @@ AS Details ) SELECT DISTINCT - 2 AS CheckID , - d.name AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Full Recovery Model w/o Log Backups' AS Finding , - 'https://BrentOzar.com/go/biglogs' AS URL , - ( 'The ' + CAST(CAST((SELECT ((SUM([mf].[size]) * 8.) / 1024.) FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = d.[database_id] AND [mf].[type_desc] = 'LOG') AS DECIMAL(18,2)) AS VARCHAR(30)) + 'MB log file has not been backed up in the last week.' ) AS Details - FROM master.sys.databases d - WHERE d.recovery_model IN ( 1, 2 ) - AND d.database_id NOT IN ( 2, 3 ) - AND d.source_database_id IS NULL - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 2) - AND NOT EXISTS ( SELECT * - FROM msdb.dbo.backupset b - WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'L' - AND b.backup_finish_date >= DATEADD(dd, - -7, GETDATE()) ); - END; - - - /* - Next up, we've got CheckID 8. (These don't have to go in order.) This one - won't work on SQL Server 2005 because it relies on a new DMV that didn't - exist prior to SQL Server 2008. This means we have to check the SQL Server - version first, then build a dynamic string with the query we want to run: - */ + 175 AS CheckID , + 'TempDB' AS DatabaseName , + 170 AS Priority , + 'File Configuration' AS FindingsGroup , + 'TempDB Has >16 Data Files' AS Finding , + 'https://www.brentozar.com/go/tempdb' AS URL , + 'Woah, Nelly! TempDB has ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + '. Did you forget to terminate a loop somewhere?' AS Details + FROM sys.[master_files] AS [mf] + WHERE [mf].[database_id] = 2 AND [mf].[type] = 0 + HAVING COUNT_BIG(*) > 16; + END; - IF NOT EXISTS ( SELECT 1 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 176 ) + BEGIN + + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_xe_sessions' ) + + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 176) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 176 AS CheckID , + '' AS DatabaseName , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'Extended Events Hyperextension' AS Finding , + 'https://www.brentozar.com/go/xe' AS URL , + 'Hey big spender, you have ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + ' Extended Events sessions running. You sure you meant to do that?' AS Details + FROM sys.dm_xe_sessions + WHERE [name] NOT IN + ( 'AlwaysOn_health', + 'system_health', + 'telemetry_xevents', + 'sp_server_diagnostics', + 'sp_server_diagnostics session', + 'hkenginexesession' ) + AND name NOT LIKE '%$A%' + HAVING COUNT_BIG(*) >= 2; + END; + END; + + /*Harmful startup parameter*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 177 ) + BEGIN + + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_registry' ) + + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 177) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 177 AS CheckID , + '' AS DatabaseName , + 5 AS Priority , + 'Monitoring' AS FindingsGroup , + 'Disabled Internal Monitoring Features' AS Finding , + 'https://msdn.microsoft.com/en-us/library/ms190737.aspx' AS URL , + 'You have -x as a startup parameter. You should head to the URL and read more about what it does to your system.' AS Details + FROM + [sys].[dm_server_registry] AS [dsr] + WHERE + [dsr].[registry_key] LIKE N'%MSSQLServer\Parameters' + AND [dsr].[value_data] = '-x';; + END; + END; + + + /* Reliability - Dangerous Third Party Modules - 179 */ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 8 ) + WHERE DatabaseName IS NULL AND CheckID = 179 ) BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 8) WITH NOWAIT; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 179) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, Priority, - FindingsGroup, - Finding, URL, - Details) - SELECT 8 AS CheckID, - 230 AS Priority, - ''Security'' AS FindingsGroup, - ''Server Audits Running'' AS Finding, - ''https://BrentOzar.com/go/audits'' AS URL, - (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status 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.'; - - EXECUTE(@StringToExecute); - END; + SELECT + 179 AS [CheckID] , + 5 AS [Priority] , + 'Reliability' AS [FindingsGroup] , + 'Dangerous Third Party Modules' AS [Finding] , + 'https://support.microsoft.com/en-us/kb/2033238' AS [URL] , + ( COALESCE(company, '') + ' - ' + COALESCE(description, '') + ' - ' + COALESCE(name, '') + ' - suspected dangerous third party module is installed.') AS [Details] + FROM sys.dm_os_loaded_modules + WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') OR UPPER(name) LIKE '%MFEBOPK.SYS' /* McAfee VirusScan Enterprise */ + OR UPPER(name) LIKE UPPER('%\HIPI.DLL') OR UPPER(name) LIKE UPPER('%\HcSQL.dll') OR UPPER(name) LIKE UPPER('%\HcApi.dll') OR UPPER(name) LIKE UPPER('%\HcThe.dll') /* McAfee Host Intrusion */ + OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') OR UPPER(name) LIKE UPPER('%\SOPHOS~%.dll') /* Sophos AV */ + OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL') /* OSISoft PI data access */ + OR UPPER(name) LIKE UPPER('%ScriptControl%.dll') OR UPPER(name) LIKE UPPER('%umppc%.dll') /* CrowdStrike */ + OR UPPER(name) LIKE UPPER('%perfiCrcPerfMonMgr.DLL') /* Trend Micro OfficeScan */ + OR UPPER(name) LIKE UPPER('%NLEMSQL.SYS') /* NetLib Encryptionizer-Software. */ + OR UPPER(name) LIKE UPPER('%MFETDIK.SYS') /* McAfee Anti-Virus Mini-Firewall */ + OR UPPER(name) LIKE UPPER('%ANTIVIRUS%'); /* To pick up sqlmaggieAntiVirus_64.dll (malware) or anything else labelled AntiVirus */ + /* MS docs link for blacklisted modules: https://learn.microsoft.com/en-us/troubleshoot/sql/performance/performance-consistency-issues-filter-drivers-modules */ END; - /* - But what if you need to run a query in every individual database? - Hop down to the @CheckUserDatabaseObjects section. - - And that's the basic idea! You can read through the rest of the - checks if you like - some more exciting stuff happens closer to the - end of the stored proc, where we start doing things like checking - the plan cache, but those aren't as cleanly commented. - - If you'd like to contribute your own check, use one of the check - formats shown above and email it to Help@BrentOzar.com. You don't - have to pick a CheckID or a link - we'll take care of that when we - test and publish the code. Thanks! - */ - + /*Find shrink database tasks*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 93 ) + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 180 ) + AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 180) WITH NOWAIT; + + WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) + ,[maintenance_plan_steps] AS ( + SELECT [name] + , [id] -- ID required to link maintenace plan with jobs and jobhistory (sp_Blitz Issue #776) + , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] + FROM [msdb].[dbo].[sysssispackages] + WHERE [packagetype] = 6 + ) + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT + 180 AS [CheckID] , + -- sp_Blitz Issue #776 + -- Job has history and was executed in the last 30 days + CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN + 100 + ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) + 200 + END AS Priority, + 'Performance' AS [FindingsGroup] , + 'Shrink Database Step In Maintenance Plan' AS [Finding] , + 'https://www.brentozar.com/go/autoshrink' AS [URL] , + 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' + + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS [Details] + FROM [maintenance_plan_steps] [mps] + CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) + join msdb.dbo.sysmaintplan_subplans as sms + on mps.id = sms.plan_id + JOIN msdb.dbo.sysjobs j + on sms.job_id = j.job_id + LEFT OUTER JOIN msdb.dbo.sysjobsteps AS step + ON j.job_id = step.job_id + LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc + ON j.job_id = sjsc.job_id + LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc + ON sjsc.schedule_id = ssc.schedule_id + AND sjsc.job_id = j.job_id + LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh + ON j.job_id = sjh.job_id + AND step.step_id = sjh.step_id + AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date + AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time + WHERE [c].[value]('(@dts:ObjectName)', 'VARCHAR(128)') = 'Shrink Database Task'; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 93) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 93 AS CheckID , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backing Up to Same Drive Where Databases Reside' AS Finding , - 'https://BrentOzar.com/go/backup' AS URL , - CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' - + UPPER(LEFT(bmf.physical_device_name, 3)) - + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details - FROM msdb.dbo.backupmediafamily AS bmf - INNER JOIN msdb.dbo.backupset AS bs ON bmf.media_set_id = bs.media_set_id - AND bs.backup_start_date >= ( DATEADD(dd, - -14, GETDATE()) ) - /* Filter out databases that were recently restored: */ - LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name AND rh.restore_date > DATEADD(dd, -14, GETDATE()) - WHERE UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( - SELECT DISTINCT - UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) - FROM sys.master_files AS mf ) - AND rh.destination_database_name IS NULL - GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 119 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_database_encryption_keys' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 119) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, DatabaseName, URL, Details) - SELECT 119 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''TDE Certificate Not Backed Up Recently'' AS Finding, - db_name(dek.database_id) AS DatabaseName, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 202 ) - AND EXISTS ( SELECT * - FROM sys.all_columns c - WHERE c.name = 'pvt_key_last_backup_date' ) - AND EXISTS ( SELECT * - FROM msdb.INFORMATION_SCHEMA.COLUMNS c - WHERE c.TABLE_NAME = 'backupset' AND c.COLUMN_NAME = 'encryptor_thumbprint' ) - BEGIN + /*Find repetitive maintenance tasks*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 181 ) + AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 181) WITH NOWAIT; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT; + WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) + ,[maintenance_plan_steps] AS ( + SELECT [name] + , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] + FROM [msdb].[dbo].[sysssispackages] + WHERE [packagetype] = 6 + ), [maintenance_plan_table] AS ( + SELECT [mps].[name] + ,[c].[value]('(@dts:ObjectName)', 'NVARCHAR(128)') AS [step_name] + FROM [maintenance_plan_steps] [mps] + CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) + ), [mp_steps_pretty] AS (SELECT DISTINCT [m1].[name] , + STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] + FOR XML PATH(N'')), 1, 2, N'') AS [maintenance_plan_steps] + FROM [maintenance_plan_table] AS [m1]) + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 181 AS [CheckID] , + 100 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Repetitive Steps In Maintenance Plans' AS [Finding] , + 'https://ola.hallengren.com/' AS [URL] , + 'The maintenance plan ' + [m].[name] + ' is doing repetitive work on indexes and statistics. Perhaps it''s time to try something more modern?' AS [Details] + FROM [mp_steps_pretty] m + WHERE m.[maintenance_plan_steps] LIKE '%Rebuild%Reorganize%' + OR m.[maintenance_plan_steps] LIKE '%Rebuild%Update%'; - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 202 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''Encryption Certificate Not Backed Up Recently'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database backups. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c - INNER JOIN msdb.dbo.backupset bs ON c.thumbprint = bs.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); END; + - - IF NOT EXISTS ( SELECT 1 + /* Reliability - No Failover Cluster Nodes Available - 184 */ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 3 ) + WHERE DatabaseName IS NULL AND CheckID = 184 ) + AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '10%' + AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '9%' BEGIN - IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 3) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 3 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Not Purged' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 + 184 AS CheckID , + 20 AS Priority , + ''Reliability'' AS FindingsGroup , + ''No Failover Cluster Nodes Available'' AS Finding , + ''https://www.brentozar.com/go/node'' AS URL , + ''There are no failover cluster nodes available if the active node fails'' AS Details + FROM ( + SELECT SUM(CASE WHEN [status] = 0 AND [is_current_owner] = 0 THEN 1 ELSE 0 END) AS [available_nodes] + FROM sys.dm_os_cluster_nodes + ) a + WHERE [available_nodes] < 1 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.'; + + EXECUTE(@StringToExecute); END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 186 ) - BEGIN - IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) + /* Reliability - TempDB File Error */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 191 ) + AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> (SELECT COUNT(*) FROM tempdb.sys.database_files) + /* User may have no permissions to see tempdb files in sys.master_files. In that case count returned will be 0 and we want to skip the check */ + AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> 0 + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 191) WITH NOWAIT + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 191 AS [CheckID] , + 50 AS [Priority] , + 'Reliability' AS [FindingsGroup] , + 'TempDB File Error' AS [Finding] , + 'https://www.brentozar.com/go/tempdboops' AS [URL] , + 'Mismatch between the number of TempDB files in sys.master_files versus tempdb.sys.database_files' AS [Details]; + END; - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 186) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details +/*Perf - Odd number of cores in a socket*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 198 ) + AND EXISTS ( SELECT 1 + FROM sys.dm_os_schedulers + WHERE is_online = 1 + AND scheduler_id < 255 + AND parent_node_id < 64 + GROUP BY parent_node_id, + is_online + HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 198) WITH NOWAIT + + INSERT INTO #BlitzResults + ( + CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details + ) + SELECT 198 AS CheckID, + NULL AS DatabaseName, + 10 AS Priority, + 'Performance' AS FindingsGroup, + 'CPU w/Odd Number of Cores' AS Finding, + 'https://www.brentozar.com/go/oddity' AS URL, + 'Node ' + CONVERT(VARCHAR(10), parent_node_id) + ' has ' + CONVERT(VARCHAR(10), COUNT(cpu_id)) + + CASE WHEN COUNT(cpu_id) = 1 THEN ' core assigned to it. This is a really bad NUMA configuration.' + ELSE ' cores assigned to it. This is a really bad NUMA configuration.' + END AS Details + FROM sys.dm_os_schedulers + WHERE is_online = 1 + AND scheduler_id < 255 + AND parent_node_id < 64 + AND EXISTS ( + SELECT 1 + FROM ( SELECT memory_node_id, SUM(online_scheduler_count) AS schedulers + FROM sys.dm_os_nodes + WHERE memory_node_id < 64 + GROUP BY memory_node_id ) AS nodes + HAVING MIN(nodes.schedulers) <> MAX(nodes.schedulers) ) - SELECT TOP 1 - 186 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Purged Too Frequently' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history only retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; - END; + GROUP BY parent_node_id, + is_online + HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; + + END; - IF NOT EXISTS ( SELECT 1 +/*Begin: checking default trace for odd DBCC activity*/ + + --Grab relevant event data + IF @TraceFileIssue = 0 + BEGIN + SELECT UPPER( + REPLACE( + SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, + ISNULL( + NULLIF( + CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), + 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. + , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), + ISNULL( + NULLIF( + CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) + , 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ) + , '') --This replaces any optional WITH clause to a DBCC command, like tableresults. + ) AS [dbcc_event_trunc_upper], + UPPER( + REPLACE( + CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), + ISNULL( + NULLIF( + CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) + , 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ), '')) AS [dbcc_event_full_upper], + MIN(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS min_start_time, + MAX(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS max_start_time, + t.NTUserName AS [nt_user_name], + t.NTDomainName AS [nt_domain_name], + t.HostName AS [host_name], + t.ApplicationName AS [application_name], + t.LoginName [login_name], + t.DBUserName AS [db_user_name] + INTO #dbcc_events_from_trace + FROM #fnTraceGettable AS t + WHERE t.EventClass = 116 + OPTION(RECOMPILE) + END; + + /*Overall count of DBCC events excluding silly stuff*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 178 ) - AND EXISTS (SELECT * - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */) + WHERE DatabaseName IS NULL AND CheckID = 203 ) + AND @TraceFileIssue = 0 BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 178) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 178 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Snapshot Backups Occurring' AS Finding , - 'https://BrentOzar.com/go/snaps' AS URL , - ( CAST(COUNT(*) AS VARCHAR(20)) + ' snapshot-looking backups have occurred in the last two weeks, indicating that IO may be freezing up.') AS Details - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()); /* In the last 2 weeks */ - END; + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 203 AS CheckID , + 50 AS Priority , + 'DBCC Events' AS FindingsGroup , + 'Overall Events' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. This does not include CHECKDB and other usually benign DBCC events.' + AS Details + FROM #dbcc_events_from_trace d + /* This WHERE clause below looks horrible, but it's because users can run stuff like + DBCC LOGINFO + with lots of spaces (or carriage returns, or comments) in between the DBCC and the + command they're trying to run. See Github issues 1062, 1074, 1075. + */ + WHERE d.dbcc_event_full_upper NOT LIKE '%DBCC%ADDINSTANCE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%AUTOPILOT%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKALLOC%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCATALOG%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCONSTRAINTS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKDB%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKFILEGROUP%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKIDENT%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKPRIMARYFILE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKTABLE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CLEANTABLE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%DBINFO%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%ERRORLOG%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INCREMENTINSTANCE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INPUTBUFFER%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%LOGINFO%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%OPENTRAN%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SETINSTANCE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOWFILESTATS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOW_STATISTICS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%NETSTATS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%LOGSPACE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEON%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEOFF%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACESTATUS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%USEROPTIONS%' + AND d.application_name NOT LIKE 'Critical Care(R) Collector' + AND d.application_name NOT LIKE '%Red Gate Software Ltd SQL Prompt%' + AND d.application_name NOT LIKE '%Spotlight Diagnostic Server%' + AND d.application_name NOT LIKE '%SQL Diagnostic Manager%' + AND d.application_name NOT LIKE 'SQL Server Checkup%' + AND d.application_name NOT LIKE '%Sentry%' + AND d.application_name NOT LIKE '%LiteSpeed%' + AND d.application_name NOT LIKE '%SQL Monitor - Monitoring%' + - IF NOT EXISTS ( SELECT 1 + HAVING COUNT(*) > 0; + + END; + + /*Check for someone running drop clean buffers*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 4 ) + WHERE DatabaseName IS NULL AND CheckID = 207 ) + AND @TraceFileIssue = 0 BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 4) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 207) WITH NOWAIT - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 4 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Sysadmins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.sysadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0 - AND l.name NOT LIKE 'NT SERVICE\%' - AND l.name <> 'l_certSignSmDetach'; /* Added in SQL 2016 */ + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 207 AS CheckID , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'DBCC DROPCLEANBUFFERS Ran Recently' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. If this is a production box, know that you''re clearing all data out of memory when this happens. What kind of monster would do that?' + AS Details + FROM #dbcc_events_from_trace d + WHERE d.dbcc_event_full_upper = N'DBCC DROPCLEANBUFFERS' + GROUP BY COALESCE(d.nt_user_name, d.login_name) + HAVING COUNT(*) > 0; + END; - IF NOT EXISTS ( SELECT 1 + /*Check for someone running free proc cache*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 5 ) + WHERE DatabaseName IS NULL AND CheckID = 208 ) + AND @TraceFileIssue = 0 BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 5) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 208) WITH NOWAIT - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 5 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Security Admins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.securityadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0; + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 208 AS CheckID , + 10 AS Priority , + 'DBCC Events' AS FindingsGroup , + 'DBCC FREEPROCCACHE Ran Recently' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. This has bad idea jeans written all over its butt, like most other bad idea jeans.' + AS Details + FROM #dbcc_events_from_trace d + WHERE d.dbcc_event_full_upper = N'DBCC FREEPROCCACHE' + GROUP BY COALESCE(d.nt_user_name, d.login_name) + HAVING COUNT(*) > 0; + END; - IF NOT EXISTS ( SELECT 1 + /*Check for someone clearing wait stats*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 104 ) + WHERE DatabaseName IS NULL AND CheckID = 205 ) + AND @TraceFileIssue = 0 BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 104) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT - INSERT INTO #BlitzResults - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] - ) - SELECT 104 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Login Can Control Server' AS [Finding] , - 'https://BrentOzar.com/go/sa' AS [URL] , - 'Login [' + pri.[name] - + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] - FROM sys.server_principals AS pri - WHERE pri.[principal_id] IN ( - SELECT p.[grantee_principal_id] - FROM sys.server_permissions AS p - WHERE p.[state] IN ( 'G', 'W' ) - AND p.[class] = 100 - AND p.[type] = 'CL' ) - AND pri.[name] NOT LIKE '##%##'; + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 205 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingsGroup , + 'Wait Stats Cleared Recently' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. Why are you clearing wait stats? What are you hiding?' + AS Details + FROM #dbcc_events_from_trace d + WHERE d.dbcc_event_full_upper = N'DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR)' + GROUP BY COALESCE(d.nt_user_name, d.login_name) + HAVING COUNT(*) > 0; + END; - IF NOT EXISTS ( SELECT 1 + /*Check for someone writing to pages. Yeah, right?*/ + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 6 ) + WHERE DatabaseName IS NULL AND CheckID = 209 ) + AND @TraceFileIssue = 0 BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 209) WITH NOWAIT - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 6 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Jobs Owned By Users' AS Finding , - 'https://BrentOzar.com/go/owners' AS URL , - ( 'Job [' + j.name + '] is owned by [' - + SUSER_SNAME(j.owner_sid) - + '] - 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); - END; + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 209 AS CheckID , + 10 AS Priority , + 'Reliability' AS FindingsGroup , + 'DBCC WRITEPAGE Used Recently' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. So, uh, are they trying to fix corruption, or cause corruption?' + AS Details + FROM #dbcc_events_from_trace d + WHERE d.dbcc_event_trunc_upper = N'DBCC WRITEPAGE' + GROUP BY COALESCE(d.nt_user_name, d.login_name) + HAVING COUNT(*) > 0; + END; - /* --TOURSTOP06-- */ - IF NOT EXISTS ( SELECT 1 + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 7 ) + WHERE DatabaseName IS NULL AND CheckID = 210 ) + AND @TraceFileIssue = 0 BEGIN - /* --TOURSTOP02-- */ - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 7) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 210) WITH NOWAIT - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 7 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Stored Procedure Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Stored procedure [master].[' - + r.SPECIFIC_SCHEMA + '].[' - + r.SPECIFIC_NAME - + '] runs automatically when SQL Server starts up. Make sure you know exactly what this stored procedure is doing, because it could pose a security risk.' ) AS Details - FROM master.INFORMATION_SCHEMA.ROUTINES r - WHERE OBJECTPROPERTY(OBJECT_ID(ROUTINE_NAME), - 'ExecIsStartup') = 1; - END; + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 210 AS CheckID , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'DBCC SHRINK% Ran Recently' AS Finding , + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. So, uh, are they trying to cause bad performance on purpose?' + AS Details + FROM #dbcc_events_from_trace d + WHERE d.dbcc_event_trunc_upper LIKE N'DBCC SHRINK%' + GROUP BY COALESCE(d.nt_user_name, d.login_name) + HAVING COUNT(*) > 0; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 10 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 10) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 10 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Resource Governor Enabled'' AS Finding, - ''https://BrentOzar.com/go/rg'' AS URL, - (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1 OPTION (RECOMPILE);'; + END; - 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.'; +/*End: checking default trace for odd DBCC activity*/ + + /*Begin check for autoshrink events*/ - EXECUTE(@StringToExecute); - END; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 206 ) + AND @TraceFileIssue = 0 + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 206) WITH NOWAIT + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT 206 AS CheckID , + 10 AS Priority , + 'Performance' AS FindingsGroup , + 'Auto-Shrink Ran Recently' AS Finding , + '' AS URL , + N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' + + CONVERT(NVARCHAR(10), COUNT(*)) + + N' auto shrink events between ' + + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) + + ' that lasted on average ' + + CONVERT(NVARCHAR(10), AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime))) + + ' seconds.' AS Details + FROM #fnTraceGettable AS t + WHERE t.EventClass IN (94, 95) + GROUP BY t.DatabaseName + HAVING AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime)) > 5; + + END; - IF NOT EXISTS ( SELECT 1 + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 11 ) + WHERE DatabaseName IS NULL AND CheckID = 215 ) + AND @TraceFileIssue = 0 + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'database_id' AND object_id = OBJECT_ID('sys.dm_exec_sessions')) BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 215) WITH NOWAIT + + SET @StringToExecute = 'INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] ) + + SELECT 215 AS CheckID , + 100 AS Priority , + ''Performance'' AS FindingsGroup , + ''Implicit Transactions'' AS Finding , + DB_NAME(s.database_id) AS DatabaseName, + ''https://www.brentozar.com/go/ImplicitTransactions/'' AS URL , + N''The database '' + + DB_NAME(s.database_id) + + '' has '' + + CONVERT(NVARCHAR(20), COUNT_BIG(*)) + + '' open implicit transactions with an oldest begin time of '' + + CONVERT(NVARCHAR(30), MIN(tat.transaction_begin_time)) + + '' Run sp_BlitzWho and check the is_implicit_transaction column to see the culprits.'' AS details + FROM sys.dm_tran_active_transactions AS tat + LEFT JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + LEFT JOIN sys.dm_exec_sessions AS s + ON s.session_id = tst.session_id + WHERE tat.name = ''implicit_transaction'' + GROUP BY DB_NAME(s.database_id), transaction_type, transaction_state;'; + + + 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.'; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 11) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 11 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Server Triggers Enabled'' AS Finding, - ''https://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);'; - - 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.'; + EXECUTE(@StringToExecute); - EXECUTE(@StringToExecute); - END; - END; + + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 12 ) + WHERE DatabaseName IS NULL AND CheckID = 221 ) BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 221) WITH NOWAIT; + + WITH reboot_airhorn + AS + ( + SELECT create_date + FROM sys.databases + WHERE database_id = 2 + UNION ALL + SELECT CAST(DATEADD(SECOND, ( ms_ticks / 1000 ) * ( -1 ), GETDATE()) AS DATETIME) + FROM sys.dm_os_sys_info + ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 221 AS CheckID, + 10 AS Priority, + 'Reliability' AS FindingsGroup, + 'Server restarted in last 24 hours' AS Finding, + '' AS URL, + 'Surprise! Your server was last restarted on: ' + CONVERT(VARCHAR(30), MAX(reboot_airhorn.create_date)) AS details + FROM reboot_airhorn + HAVING MAX(reboot_airhorn.create_date) >= DATEADD(HOUR, -24, GETDATE()); + - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 12) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 12 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Close Enabled' AS Finding , - 'https://BrentOzar.com/go/autoclose' AS URL , - ( 'Database [' + [name] - + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_close_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 12); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 13 ) + WHERE DatabaseName IS NULL AND CheckID = 229 ) + AND CAST(SERVERPROPERTY('Edition') AS NVARCHAR(4000)) LIKE '%Evaluation%' BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 13) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 229) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 229 AS CheckID, + 1 AS Priority, + 'Reliability' AS FindingsGroup, + 'Evaluation Edition' AS Finding, + 'https://www.BrentOzar.com/go/workgroup' AS URL, + 'This server will stop working on: ' + CAST(CONVERT(DATETIME, DATEADD(DD, 180, create_date), 102) AS VARCHAR(100)) AS details + FROM sys.server_principals + WHERE sid = 0x010100000000000512000000; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 13 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Enabled' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - ( 'Database [' + [name] - + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_shrink_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 13); END; + + + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 14 ) + WHERE DatabaseName IS NULL AND CheckID = 233 ) BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 233) WITH NOWAIT; + + + IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_os_memory_clerks') AND name = 'pages_kb') BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 14) WITH NOWAIT; + /* SQL 2012+ version */ + SET @StringToExecute = N' + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 233 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + ''https://www.BrentOzar.com/go/userstore'' AS URL, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / SUM(pages_kb) >= 0.1 + AND SUM(pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + ELSE + BEGIN + /* Antiques Roadshow SQL 2008R2 - version */ + SET @StringToExecute = N' + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 233 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + ''https://www.BrentOzar.com/go/userstore'' AS URL, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / SUM(single_pages_kb + multi_pages_kb) >= 0.1 + AND SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 14 AS CheckID, - [name] as DatabaseName, - 50 AS Priority, - ''Reliability'' AS FindingsGroup, - ''Page Verification Not Optimal'' AS Finding, - ''https://BrentOzar.com/go/torn'' AS URL, - (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details - FROM sys.databases - WHERE page_verify_option < 2 - AND name <> ''tempdb'' - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 14) 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.'; - - EXECUTE(@StringToExecute); - END; END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 15 ) - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 15) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 15 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Create Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/acs' AS URL , - ( 'Database [' + [name] - + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_create_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 15); - END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 16 ) + WHERE DatabaseName IS NULL AND CheckID = 234 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 16) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 234) WITH NOWAIT; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 16 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Update Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/aus' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 16); + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 234 AS CheckID, + 100 AS Priority, + db_name(f.database_id) AS DatabaseName, + 'Reliability' AS FindingsGroup, + 'SQL Server Update May Fail' AS Finding, + 'https://desertdba.com/failovers-cant-serve-two-masters/' AS URL, + 'This database has a file with a logical name of ''master'', which can break SQL Server updates. Rename it in SSMS by right-clicking on the database, go into Properties, and rename the file. Takes effect instantly.' AS details + FROM master.sys.master_files f + WHERE (f.name = N'master') + AND f.database_id > 4 + AND db_name(f.database_id) <> 'master'; /* Thanks Michaels3 for catching this */ END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 17 ) + WHERE DatabaseName IS NULL AND CheckID = 268 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 17) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 268) WITH NOWAIT; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 17 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Stats Updated Asynchronously' AS Finding , - 'https://BrentOzar.com/go/asyncstats' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_async_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 17); + 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 NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 18 ) + IF @CheckUserDatabaseObjects = 1 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 18) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 18 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Forced Parameterization On' AS Finding , - 'https://BrentOzar.com/go/forced' AS URL , - ( 'Database [' + [name] - + '] has forced parameterization enabled. SQL Server will aggressively reuse query execution plans even if the applications do not parameterize their queries. This can be a performance booster with some programming languages, or it may use universally bad execution plans when better alternatives are available for certain parameters.' ) AS Details - FROM sys.databases - WHERE is_parameterization_forced = 1 - AND name NOT IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 18); - END; + IF @Debug IN (1, 2) RAISERROR('Starting @CheckUserDatabaseObjects section.', 0, 1) WITH NOWAIT - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 20 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; + /* + But what if you need to run a query in every individual database? + Check out CheckID 99 below. Yes, it uses sp_MSforeachdb, and no, + we're not happy about that. sp_MSforeachdb is known to have a lot + of issues, like skipping databases sometimes. However, this is the + only built-in option that we have. If you're writing your own code + for database maintenance, consider Aaron Bertrand's alternative: + http://www.mssqltips.com/sqlservertip/2201/making-a-more-reliable-and-flexible-spmsforeachdb/ + We don't include that as part of sp_Blitz, of course, because + copying and distributing copyrighted code from others without their + written permission isn't a good idea. + */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 99 ) + BEGIN - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 20 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Date Correlation On' AS Finding , - 'https://BrentOzar.com/go/corr' AS URL , - ( 'Database [' + [name] - + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details - FROM sys.databases - WHERE is_date_correlation_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 20); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 21 ) - BEGIN - /* --TOURSTOP04-- */ - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 21) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 21 AS CheckID, - [name] as DatabaseName, - 200 AS Priority, - ''Informational'' AS FindingsGroup, - ''Database Encrypted'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details - FROM sys.databases - WHERE is_encrypted = 1 - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 21) 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.'; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXECUTE(@StringToExecute); - END; - END; - - /* - Believe it or not, SQL Server doesn't track the default values - for sp_configure options! We'll make our own list here. - */ + 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 + databases. We're not checking #SkipChecks here for each database to + see if we should run the check in this database. That means we may + still run a skipped check if it involves sp_MSforeachdb. We just + don't output those results in the last step. + */ - IF @Debug IN (1, 2) RAISERROR('Generating default configuration values', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 163 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + /* --TOURSTOP03-- */ - 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; - 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 ); + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 163) WITH NOWAIT; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 163, + N''?'', + 200, + ''Performance'', + ''Query Store Disabled'', + ''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)'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 22 ) - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 262 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + AND @ProductVersionMajor > 13 /* The relevant column only exists in 2017+ */ + BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 22) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 262) WITH NOWAIT; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT cd.CheckID , - 200 AS Priority , - 'Non-Default Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/conf' AS URL , - ( 'This sp_configure option has been changed. Its default value is ' - + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), - '(unknown)') - + ' and it has been set to ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '.' ) AS Details - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - LEFT OUTER JOIN #ConfigurationDefaults cdUsed ON cdUsed.name = cr.name - AND cdUsed.DefaultValue = cr.value_in_use - WHERE cdUsed.name IS NULL; - END; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 262, + N''?'', + 200, + ''Performance'', + ''Query Store Wait Stats Disabled'', + ''https://www.sqlskills.com/blogs/erin/query-store-settings/'', + (''The new SQL Server 2017 Query Store feature for tracking wait stats has not been enabled on this database. It is very useful for tracking wait stats at a query level.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND wait_stats_capture_mode = 0 + OPTION (RECOMPILE)'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 263 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 263) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 190 ) - BEGIN + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 263, + N''?'', + 200, + ''Performance'', + ''Query Store Effectively Disabled'', + ''https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify'', + (''Query Store is not in a state where it is writing, so it is effectively disabled. Check your Query Store settings.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND actual_state <> 2 + OPTION (RECOMPILE)'; + END; - IF @Debug IN (1, 2) RAISERROR('Setting @MinServerMemory and @MaxServerMemory', 0, 1) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 264 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN - SELECT @MinServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'min server memory (MB)'; - SELECT @MaxServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'max server memory (MB)'; - - IF (@MinServerMemory = @MaxServerMemory) - BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 264) WITH NOWAIT; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 190) WITH NOWAIT; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 264, + N''?'', + 200, + ''Performance'', + ''Undesired Query Store State'', + ''https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify'', + (''You have asked for Query Store to be in '' + desired_state_desc + '' mode, but it is in '' + actual_state_desc + '' mode.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND desired_state <> actual_state + OPTION (RECOMPILE)'; + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES - ( 190, - 200, - 'Performance', - 'Non-Dynamic Memory', - 'https://BrentOzar.com/go/memory', - 'Minimum Server Memory setting is the same as the Maximum (both set to ' + CAST(@MinServerMemory AS NVARCHAR(50)) + '). This will not allow dynamic memory. Please revise memory settings' - ); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 188 ) - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 265 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 265) WITH NOWAIT; - /* Let's set variables so that our query is still SARGable */ - - IF @Debug IN (1, 2) RAISERROR('Setting @Processors.', 0, 1) WITH NOWAIT; - - SET @Processors = (SELECT cpu_count FROM sys.dm_os_sys_info); - - IF @Debug IN (1, 2) RAISERROR('Setting @NUMANodes', 0, 1) WITH NOWAIT; - - SET @NUMANodes = (SELECT COUNT(1) - FROM sys.dm_os_performance_counters pc - WHERE pc.object_name LIKE '%Buffer Node%' - AND counter_name = 'Page life expectancy'); - /* If Cost Threshold for Parallelism is default then flag as a potential issue */ - /* If MAXDOP is default and processors > 8 or NUMA nodes > 1 then flag as potential issue */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 188) WITH NOWAIT; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 265, + N''?'', + 200, + ''Performance'', + ''Query Store Unusually Configured'', + ''https://www.sqlskills.com/blogs/erin/query-store-best-practices/'', + (''The '' + query_capture_mode_desc + '' query capture mode '' + + CASE query_capture_mode_desc + WHEN ''ALL'' THEN ''captures more data than you will probably use. If your workload is heavily ad-hoc, then it can also cause Query Store to capture so much that it turns itself off.'' + WHEN ''NONE'' THEN ''stops Query Store capturing data for new queries.'' + WHEN ''CUSTOM'' THEN ''suggests that somebody has gone out of their way to only capture exactly what they want.'' + ELSE ''is not documented.'' END) + 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'' + OPTION (RECOMPILE)'; + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 188 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/cxpacket' AS URL , - ( 'Set to ' + CAST(cr.value_in_use AS NVARCHAR(50)) + ', its default value. Changing this sp_configure setting may reduce CXPACKET waits.') - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - AND cr.value_in_use = cd.DefaultValue - WHERE cr.name = 'cost threshold for parallelism' - OR (cr.name = 'max degree of parallelism' AND (@NUMANodes > 1 OR @Processors > 8)); - END; + IF @ProductVersionMajor = 13 AND @ProductVersionMinor < 2149 --2016 CU1 has the fix in it + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 182 ) + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 182) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 + 182, + ''Server'', + 20, + ''Reliability'', + ''Query Store Cleanup Disabled'', + ''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);'; + + 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.'; + + EXECUTE(@StringToExecute); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 235 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 24 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 24) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 24 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'System Database on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) IN ( 'master', - 'model', 'msdb' ); - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 235) WITH NOWAIT; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 235, + N''?'', + 150, + ''Performance'', + ''Inconsistent Query Store metadata'', + '''', + (''Query store state in master metadata and database specific metadata not in sync.'') + 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)'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 25 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 25) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 41 ) + BEGIN - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 25 AS CheckID , - 'tempdb' , - 20 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - CASE WHEN growth > 0 - THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) - ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) - END AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) = 'tempdb'; - END; - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 41) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'use [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 41, + N''?'', + 170, + ''File Configuration'', + ''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]'' + GROUP BY LEFT(physical_name, 1) + HAVING COUNT(*) > 1 + AND SUM(size) < 268435456 OPTION (RECOMPILE);'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 26 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 26) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 42 ) + BEGIN - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 26 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 20 AS Priority , - 'Reliability' AS FindingsGroup , - 'User Databases on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) NOT IN ( 'master', - 'model', 'msdb', - 'tempdb' ) - AND DB_NAME(database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 26 ); - END; - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 42) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'use [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 42, + N''?'', + 170, + ''File Configuration'', + ''Uneven File Growth Settings in One Filegroup'', + ''https://www.brentozar.com/go/grow'', + (''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'' + GROUP BY data_space_id + HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 27 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 27) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 'master' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Master Database' AS Finding , - 'https://BrentOzar.com/go/mastuser' AS URL , - ( 'The ' + name - + ' table in the master database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. 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; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 82 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 82) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 28 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 28) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 28 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the MSDB Database' AS Finding , - 'https://BrentOzar.com/go/msdbuser' AS URL , - ( 'The ' + name - + ' table in the msdb database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the msdb database may not be restored in the event of a disaster.' ) AS Details - FROM msdb.sys.tables - WHERE is_ms_shipped = 0 AND name NOT LIKE '%DTA_%'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 29 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 29) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 29 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Model Database' AS Finding , - 'https://BrentOzar.com/go/model' AS URL , - ( 'The ' + name - + ' table in the model database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the model database are automatically copied into all new databases.' ) AS Details - FROM model.sys.tables - WHERE is_ms_shipped = 0; - END; + EXEC sp_MSforeachdb 'use [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, Details) + SELECT DISTINCT 82 AS CheckID, + N''?'' as DatabaseName, + 170 AS Priority, + ''File Configuration'' AS FindingsGroup, + ''File growth set to percent'', + ''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);'; + END; + /* addition by Henrik Staun Poulsen, Stovi Software */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 158 ) + BEGIN - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 30 ) - BEGIN - IF ( SELECT COUNT(*) - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 - ) < 7 + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 158) WITH NOWAIT; - BEGIN + EXEC sp_MSforeachdb 'use [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, Details) + SELECT DISTINCT 158 AS CheckID, + N''?'' as DatabaseName, + 170 AS Priority, + ''File Configuration'' AS FindingsGroup, + ''File growth set to 1MB'', + ''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);'; + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 30) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 33 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + AND @SkipBlockingChecks = 0 + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 33) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 33, + db_name(), + 200, + ''Licensing'', + ''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);'; + END; + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 30 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Not All Alerts Configured' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - END; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 19 ) + BEGIN + /* Method 1: Check sys.databases parameters */ + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 19) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 19 AS CheckID , + [name] AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Replication In Use' AS Finding , + 'https://www.brentozar.com/go/repl' AS URL , + ( 'Database [' + [name] + + '] is a replication publisher, subscriber, or distributor.' ) AS Details + FROM sys.databases + WHERE name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 19) + AND (is_published = 1 + OR is_subscribed = 1 + OR is_merge_published = 1 + OR is_distributor = 1); + /* Method B: check subscribers for MSreplication_objects tables */ + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 19, + db_name(), + 200, + ''Informational'', + ''Replication In Use'', + ''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)'; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 59 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE enabled = 1 - AND COALESCE(has_notification, 0) = 0 - AND (job_id IS NULL OR job_id = 0x)) + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 32 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 59) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 59 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Configured without Follow Up' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 32) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 32, + N''?'', + 150, + ''Performance'', + ''Triggers on Tables'', + ''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'' + HAVING SUM(1) > 0 OPTION (RECOMPILE)'; END; - END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 96 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE message_id IN ( 823, 824, 825 ) ) - - BEGIN; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 96) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 96 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Corruption' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; - - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 61 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 61) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 61 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Sev 19-25' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; - - END; - - END; - - --check for disabled alerts - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 98 ) - BEGIN - IF EXISTS ( SELECT name - FROM msdb.dbo.sysalerts - WHERE enabled = 0 ) - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 164 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'fn_validate_plan_guide') BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 98) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 98 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Disabled' AS Finding , - 'https://www.BrentOzar.com/go/alerts/' AS URL , - ( 'The following Alert is disabled, please review and enable if desired: ' - + name ) AS Details - FROM msdb.dbo.sysalerts - WHERE enabled = 0; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 164) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SET QUOTED_IDENTIFIER ON; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 164, + N''?'', + 100, + ''Reliability'', + ''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)'; END; - - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 31 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysoperators - WHERE enabled = 1 ) - + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 46 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 31) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 31 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Operators Configured/Enabled' AS Finding , - 'https://BrentOzar.com/go/op' AS URL , - ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 46) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 46, + N''?'', + 150, + ''Performance'', + ''Leftover Fake Indexes From Wizards'', + ''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);'; END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 34 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_db_mirroring_auto_page_repair' ) + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 47 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 34) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 47) WITH NOWAIT; - SET @StringToExecute = 'INSERT INTO #BlitzResults + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5051,43 +7319,27 @@ AS Finding, URL, Details) - SELECT DISTINCT - 34 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details - FROM (SELECT rp2.database_id, rp2.modification_time - FROM sys.dm_db_mirroring_auto_page_repair rp2 - WHERE rp2.[database_id] not in ( - SELECT db2.[database_id] - FROM sys.databases as db2 - WHERE db2.[state] = 1 - ) ) as rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); + SELECT 47, + N''?'', + 100, + ''Performance'', + ''Indexes Disabled'', + ''https://www.brentozar.com/go/ixoff'', + (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1 OPTION (RECOMPILE);'; END; - END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 89 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_hadr_auto_page_repair' ) + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 48 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 89) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 48) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5095,38 +7347,27 @@ AS Finding, URL, Details) - SELECT DISTINCT - 89 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''AlwaysOn has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details - FROM sys.dm_hadr_auto_page_repair rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); + SELECT DISTINCT 48, + N''?'', + 150, + ''Performance'', + ''Foreign Keys Not Trusted'', + ''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);'; END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 90 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.sys.all_objects - WHERE name = 'suspect_pages' ) + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 56 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 90) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 56) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5134,729 +7375,1086 @@ AS Finding, URL, Details) - SELECT DISTINCT - 90 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details - FROM msdb.dbo.suspect_pages sp - INNER JOIN master.sys.databases db ON sp.database_id = db.database_id - WHERE sp.last_update_date >= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; + SELECT 56, + N''?'', + 150, + ''Performance'', + ''Check Constraint Not Trusted'', + ''https://www.brentozar.com/go/trust'', + (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') + from [?].sys.check_constraints 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 OPTION (RECOMPILE);'; + END; - 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.'; - - EXECUTE(@StringToExecute); + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 95 ) + BEGIN + IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' + AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 95) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 95 AS CheckID, + N''?'' as DatabaseName, + 110 AS Priority, + ''Performance'' AS FindingsGroup, + ''Plan Guides Enabled'' AS Finding, + ''https://www.brentozar.com/go/guides'' AS URL, + (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details + FROM [?].sys.plan_guides WHERE is_disabled = 0 OPTION (RECOMPILE);'; + END; END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 36 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 36) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 36 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Reads on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Reads are averaging longer than 200ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_read_ms / ( 1.0 + num_of_reads ) ) > 200 - AND num_of_reads > 100000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 37 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 37) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 37 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Writes on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Writes are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_write_ms / ( 1.0 - + num_of_writes ) ) > 100 - AND num_of_writes > 100000; - END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 40 ) - BEGIN - IF ( SELECT COUNT(*) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - ) = 1 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 60 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 40) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 40 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Only Has 1 Data File' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' - ); + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 60) WITH NOWAIT; + + EXEC sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 60 AS CheckID, + N''?'' as DatabaseName, + 100 AS Priority, + ''Performance'' AS FindingsGroup, + ''Fill Factor Changed'', + ''https://www.brentozar.com/go/fillfactor'' AS URL, + ''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' objects with fill factor = '' + CAST(fill_factor AS NVARCHAR(5)) + ''%. This can cause memory and storage performance problems, but may also prevent page splits.'' + FROM [?].sys.indexes + WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 + GROUP BY fill_factor OPTION (RECOMPILE);'; END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 183 ) - - BEGIN - IF ( SELECT COUNT (distinct [size]) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - HAVING MAX((size * 8) / (1024. * 1024)) - MIN((size * 8) / (1024. * 1024)) > 1. - ) <> 1 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 78 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 183) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 78) WITH NOWAIT; + + EXECUTE master.sys.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #Recompile + SELECT DISTINCT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA + FROM sys.sql_modules AS SM + LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() + LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' + LEFT OUTER JOIN INFORMATION_SCHEMA.ROUTINES AS ISR on ISR.Routine_Name = SO.name AND ISR.SPECIFIC_CATALOG = DB_Name() + WHERE SM.is_recompiled=1 OPTION (RECOMPILE); /* oh the rich irony of recompile here */ + '; + INSERT INTO #BlitzResults + (Priority, + FindingsGroup, + Finding, + DatabaseName, + URL, + Details, + CheckID) + SELECT [Priority] = '100', + FindingsGroup = 'Performance', + Finding = 'Stored Procedure WITH RECOMPILE', + DatabaseName = DBName, + URL = 'https://www.brentozar.com/go/recompile', + Details = '[' + DBName + '].[' + SPSchema + '].[' + ProcName + '] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.', + CheckID = '78' + FROM #Recompile AS TR + WHERE ProcName NOT LIKE 'sp_AllNightLog%' AND ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%' AND ProcName NOT LIKE 'sp_PressureDetector' + AND DBName NOT IN ('master', 'model', 'msdb', 'tempdb'); + DROP TABLE #Recompile; + END; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 183 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Unevenly Sized Data Files' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB data files are not configured with the same size. Unevenly sized tempdb data files will result in unevenly sized workloads.' - ); + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 86 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 86) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://www.brentozar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM (SELECT memberuid = convert(int, member_principal_id), groupuid = convert(int, role_principal_id) FROM [?].sys.database_role_members) m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; END; - END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 44 ) - BEGIN + /*Check for non-aligned indexes in partioned databases*/ - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 44) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 72 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 72) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + insert into #partdb(dbname, objectname, type_desc) + SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc + FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id + JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id + LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() + WHERE o.type = ''u'' + -- Clustered and Non-Clustered indexes + AND i.type IN (1, 2) + AND o.object_id in + ( + SELECT a.object_id from + (SELECT ob.object_id, ds.type_desc from sys.objects ob JOIN sys.indexes ind on ind.object_id = ob.object_id join sys.data_spaces ds on ds.data_space_id = ind.data_space_id + GROUP BY ob.object_id, ds.type_desc ) a group by a.object_id having COUNT (*) > 1 + ) OPTION (RECOMPILE);'; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 72 AS CheckID , + dbname AS DatabaseName , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'The partitioned database ' + dbname + + ' may have non-aligned indexes' AS Finding , + 'https://www.brentozar.com/go/aligned' AS URL , + 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details + FROM #partdb + WHERE dbname IS NOT NULL + AND dbname NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 72); + DROP TABLE #partdb; + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 44 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Order Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'order hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 45 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 45) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 45 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Join Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'join hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 49 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 49) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 49 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Linked Server Configured' AS Finding , - 'https://BrentOzar.com/go/link' AS URL , - +CASE WHEN l.remote_name = 'sa' - THEN s.data_source - + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' - ELSE s.data_source - + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' - END AS Details - FROM sys.servers s - INNER JOIN sys.linked_logins l ON s.server_id = l.server_id - WHERE s.is_linked = 1; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 50 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 50) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 50 AS CheckID , - 100 AS Priority , - ''Performance'' AS FindingsGroup , - ''Max Memory Set Too High'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''SQL Server max memory is set to '' - + CAST(c.value_in_use AS VARCHAR(20)) - + '' megabytes, but the server only has '' - + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes. SQL Server may drain the system dry of memory, and under certain conditions, this can cause Windows to swap to disk.'' AS Details - FROM sys.dm_os_sys_memory m - INNER JOIN sys.configurations c ON c.name = ''max server memory (MB)'' - WHERE CAST(m.total_physical_memory_kb AS BIGINT) < ( CAST(c.value_in_use AS BIGINT) * 1024 ) 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.'; - - EXECUTE(@StringToExecute); - END; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 113 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 113) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT DISTINCT 113, + N''?'', + 50, + ''Reliability'', + ''Full Text Indexes Not Updating'', + ''https://www.brentozar.com/go/fulltext'', + (''At least one full text index in this database has not been crawled in the last week.'') + from [?].sys.fulltext_indexes i WHERE change_tracking_state_desc <> ''AUTO'' AND i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE()) OPTION (RECOMPILE);'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 51 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 115 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 51) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 51 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details - FROM sys.dm_os_sys_memory m - WHERE CAST(m.available_physical_memory_kb AS BIGINT) < 262144 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.'; - - EXECUTE(@StringToExecute); + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 115) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 115, + N''?'', + 110, + ''Performance'', + ''Parallelism Rocket Surgery'', + ''https://www.brentozar.com/go/makeparallel'', + (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') + from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'' OPTION (RECOMPILE);'; END; - END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 159 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 122 ) BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 122) WITH NOWAIT; + + /* SQL Server 2012 and newer uses temporary stats for Availability Groups, and those show up as user-created */ + IF EXISTS (SELECT * + FROM sys.all_columns c + INNER JOIN sys.all_objects o ON c.object_id = o.object_id + WHERE c.name = 'is_temporary' AND o.name = 'stats') + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 122, + N''?'', + 200, + ''Performance'', + ''User-Created Statistics In Place'', + ''https://www.brentozar.com/go/userstats'', + (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') + from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0 + HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 159) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 159 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low in NUMA Nodes'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details - FROM sys.dm_os_nodes m - WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%'' 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.'; - - EXECUTE(@StringToExecute); - END; - END; + ELSE + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT 122, + N''?'', + 200, + ''Performance'', + ''User-Created Statistics In Place'', + ''https://www.brentozar.com/go/userstats'', + (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') + from [?].sys.stats WHERE user_created = 1 + HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 53 ) - BEGIN + END; /* IF NOT EXISTS ( SELECT 1 */ - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 53) WITH NOWAIT; + /*Check for high VLF count: this will omit any database snapshots*/ - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 53 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Cluster Node' AS Finding , - 'https://BrentOzar.com/go/node' AS URL , - 'This is a node in a cluster.' AS Details - FROM sys.dm_os_cluster_nodes; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 69 ) + BEGIN + IF @ProductVersionMajor >= 11 - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 55 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 55 AS CheckID , - [name] AS DatabaseName , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Database Owner <> SA' AS Finding , - 'https://BrentOzar.com/go/owndb' AS URL , - ( 'Database name: ' + [name] + ' ' - + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details - FROM sys.databases - WHERE SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01) - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 55); - END; + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; + + EXEC sp_MSforeachdb N'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #LogInfo2012 + EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; + IF @@ROWCOUNT > 999 + BEGIN + INSERT INTO #BlitzResults + ( CheckID + ,DatabaseName + ,Priority + ,FindingsGroup + ,Finding + ,URL + ,Details) + SELECT 69 + ,DB_NAME() + ,170 + ,''File Configuration'' + ,''High VLF Count'' + ,''https://www.brentozar.com/go/vlf'' + ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' + FROM #LogInfo2012 + WHERE EXISTS (SELECT name FROM master.sys.databases + WHERE source_database_id is null) OPTION (RECOMPILE); + END + TRUNCATE TABLE #LogInfo2012;'; + DROP TABLE #LogInfo2012; + END; + ELSE + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (pre-2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; + + EXEC sp_MSforeachdb N'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #LogInfo + EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; + IF @@ROWCOUNT > 999 + BEGIN + INSERT INTO #BlitzResults + ( CheckID + ,DatabaseName + ,Priority + ,FindingsGroup + ,Finding + ,URL + ,Details) + SELECT 69 + ,DB_NAME() + ,170 + ,''File Configuration'' + ,''High VLF Count'' + ,''https://www.brentozar.com/go/vlf'' + ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' + FROM #LogInfo + WHERE EXISTS (SELECT name FROM master.sys.databases + WHERE source_database_id is null) OPTION (RECOMPILE); + END + TRUNCATE TABLE #LogInfo;'; + DROP TABLE #LogInfo; + END; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 57 ) - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 80 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 80) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://www.brentozar.com/go/maxsize'', + (''The ['' + DB_NAME() + ''] database file '' + df.name + '' has a max file size set to '' + + CAST(CAST(df.max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') + FROM sys.database_files df + WHERE 0 = (SELECT is_read_only FROM sys.databases WHERE name = ''?'') + AND df.max_size <> 268435456 + AND df.max_size <> -1 + AND df.type <> 2 + AND df.growth > 0 + AND df.name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; + + DELETE br + FROM #BlitzResults br + INNER JOIN #SkipChecks sc ON sc.CheckID = 80 AND br.DatabaseName = sc.DatabaseName; + END; + + + /* Check if columnstore indexes are in use - for Github issue #615 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ + BEGIN + TRUNCATE TABLE #TemporaryDatabaseResults; + + 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.indexes WHERE type IN (5,6)) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; + IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @ColumnStoreIndexesInUse = 1; + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 57) WITH NOWAIT; + /* Check if Query Store is in use - for Github issue #3527 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ + AND @ProductVersionMajor > 12 /* The relevant column only exists in versions that support Query store */ + BEGIN + TRUNCATE TABLE #TemporaryDatabaseResults; + + 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 AND database_id <> 3) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; + IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @QueryStoreInUse = 1; + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 57 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'SQL Agent Job Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Job [' + j.name - + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details - FROM msdb.dbo.sysschedules sched - JOIN msdb.dbo.sysjobschedules jsched ON sched.schedule_id = jsched.schedule_id - JOIN msdb.dbo.sysjobs j ON jsched.job_id = j.job_id - WHERE sched.freq_type = 64 - AND sched.enabled = 1; - END; + /* Non-Default Database Scoped Config - Github issue #598 */ + IF EXISTS ( SELECT * FROM sys.all_objects WHERE [name] = 'database_scoped_configurations' ) + BEGIN + + 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) + 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', 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), + (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'')) + FROM [?].sys.database_scoped_configurations dsc + 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);'; + END; + /* Check 218 - Show me the dodgy SET Options */ + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 218 + ) + BEGIN + IF @Debug IN (1,2) + BEGIN + RAISERROR ('Running CheckId [%d].',0,1,218) WITH NOWAIT; + END + EXECUTE sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT 218 AS CheckID + ,''?'' AS DatabaseName + ,150 AS Priority + ,''Performance'' AS FindingsGroup + ,''Objects created with dangerous SET Options'' AS Finding + ,''https://www.brentozar.com/go/badset'' AS URL + ,''The '' + QUOTENAME(DB_NAME()) + + '' database has '' + CONVERT(VARCHAR(20),COUNT(1)) + + '' objects that were created with dangerous ANSI_NULL or QUOTED_IDENTIFIER options.'' + + '' These objects can break when using filtered indexes, indexed views'' + + '' and other advanced SQL features.'' AS Details + FROM sys.sql_modules sm + JOIN sys.objects o ON o.[object_id] = sm.[object_id] + AND ( + sm.uses_ansi_nulls <> 1 + OR sm.uses_quoted_identifier <> 1 + ) + AND o.is_ms_shipped = 0 + HAVING COUNT(1) > 0;'; + END; --of Check 218. - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 97 ) - BEGIN + /* Check 225 - Reliability - Resumable Index Operation Paused */ + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 225 + ) + AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'index_resumable_operations') + BEGIN + IF @Debug IN (1,2) + BEGIN + RAISERROR ('Running CheckId [%d].',0,1,218) WITH NOWAIT; + END - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 97) WITH NOWAIT; + EXECUTE sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT 225 AS CheckID + ,''?'' AS DatabaseName + ,200 AS Priority + ,''Reliability'' AS FindingsGroup + ,''Resumable Index Operation Paused'' AS Finding + ,''https://www.brentozar.com/go/resumable'' AS URL + ,iro.state_desc + N'' since '' + CONVERT(NVARCHAR(50), last_pause_time, 120) + '', '' + + CAST(iro.percent_complete AS NVARCHAR(20)) + ''% complete: '' + + 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;'; + END; --of Check 225. + + --/* Check 220 - Statistics Without Histograms */ + --IF NOT EXISTS ( + -- SELECT 1 + -- FROM #SkipChecks + -- WHERE DatabaseName IS NULL + -- AND CheckID = 220 + -- ) + -- AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_histogram') + --BEGIN + -- IF @Debug IN (1,2) + -- BEGIN + -- RAISERROR ('Running CheckId [%d].',0,1,220) WITH NOWAIT; + -- END + + -- EXECUTE sp_MSforeachdb 'USE [?]; + -- SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + -- INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + -- SELECT 220 AS CheckID + -- ,DB_NAME() AS DatabaseName + -- ,110 AS Priority + -- ,''Performance'' AS FindingsGroup + -- ,''Statistics Without Histograms'' AS Finding + -- ,''https://www.brentozar.com/go/brokenstats'' AS URL + -- ,CAST(COUNT(DISTINCT o.object_id) AS VARCHAR(100)) + '' tables have statistics that have not been updated since the database was restored or upgraded,'' + -- + '' and have no data in their histogram. See the More Info URL for a script to update them. '' AS Details + -- FROM sys.all_objects o + -- INNER JOIN sys.stats s ON o.object_id = s.object_id AND s.has_filter = 0 + -- OUTER APPLY sys.dm_db_stats_histogram(o.object_id, s.stats_id) h + -- 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'') + -- HAVING COUNT(DISTINCT o.object_id) > 0;'; + --END; --of Check 220. - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details + /*Check for the last good DBCC CHECKDB date */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 68 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; + + /* Removed as populating the #DBCCs table now done in advance as data is uses for multiple checks*/ + --EXEC sp_MSforeachdb N'USE [?]; + --SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + --INSERT #DBCCs + -- (ParentObject, + -- Object, + -- Field, + -- Value) + --EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); + --UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; + + WITH DB2 + AS ( SELECT DISTINCT + Field , + Value , + DbName + FROM #DBCCs + INNER JOIN sys.databases d ON #DBCCs.DbName = d.name + WHERE Field = 'dbi_dbccLastKnownGood' + AND d.create_date < DATEADD(dd, -14, GETDATE()) ) - SELECT 97 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Unusual SQL Server Edition' AS Finding , - 'https://BrentOzar.com/go/workgroup' AS URL , - ( 'This server is using ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + ', which is capped at low amounts of CPU and memory.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Data Center%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Business Intelligence%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 154 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 154) WITH NOWAIT; + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 68 AS CheckID , + DB2.DbName AS DatabaseName , + 1 AS PRIORITY , + 'Reliability' AS FindingsGroup , + 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , + 'https://www.brentozar.com/go/checkdb' AS URL , + 'Last successful CHECKDB: ' + + CASE DB2.Value + WHEN '1900-01-01 00:00:00.000' + THEN ' never.' + ELSE DB2.Value + END AS Details + FROM DB2 + WHERE DB2.DbName <> 'tempdb' + AND DB2.DbName NOT IN ( SELECT DISTINCT + DatabaseName + FROM + #SkipChecks + WHERE CheckID IS NULL OR CheckID = 68) + AND DB2.DbName NOT IN ( SELECT name + FROM sys.databases + WHERE is_read_only = 1) + AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, + -14, + CURRENT_TIMESTAMP); + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 154 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - '32-bit SQL Server Installed' AS Finding , - 'https://BrentOzar.com/go/32bit' AS URL , - ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%'; - END; + END; /* IF @CheckUserDatabaseObjects = 1 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 62 ) + IF @CheckProcedureCache = 1 + BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 62) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 62 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Old Compatibility Level' AS Finding , - 'https://BrentOzar.com/go/compatlevel' AS URL , - ( 'Database ' + [name] - + ' is compatibility level ' - + CAST(compatibility_level AS VARCHAR(20)) - + ', which may cause unwanted results when trying to run queries that have newer T-SQL features.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 62) - AND compatibility_level <= 90; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 94 ) + IF @Debug IN (1, 2) RAISERROR('Begin checking procedure cache', 0, 1) WITH NOWAIT; + BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 94 AS CheckID , - 200 AS [Priority] , - 'Monitoring' AS FindingsGroup , - 'Agent Jobs Without Failure Emails' AS Finding , - 'https://BrentOzar.com/go/alerts' AS URL , - 'The job ' + [name] - + ' has not been set up to notify an operator if it fails.' AS Details - FROM msdb.[dbo].[sysjobs] j - INNER JOIN ( SELECT DISTINCT - [job_id] - FROM [msdb].[dbo].[sysjobschedules] - WHERE next_run_date > 0 - ) s ON j.job_id = s.job_id - WHERE j.enabled = 1 - AND j.notify_email_operator_id = 0 - AND j.notify_netsend_operator_id = 0 - AND j.notify_page_operator_id = 0 - AND j.category_id <> 100; /* Exclude SSRS category */ - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 35 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 35) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 35 AS CheckID , + 100 AS Priority , + 'Performance' AS FindingsGroup , + 'Single-Use Plans in Procedure Cache' AS Finding , + 'https://www.brentozar.com/go/single' AS URL , + ( CAST(COUNT(*) AS VARCHAR(10)) + + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details + FROM sys.dm_exec_cached_plans AS cp + WHERE cp.usecounts = 1 + AND cp.objtype = 'Adhoc' + AND EXISTS ( SELECT + 1 + FROM sys.configurations + WHERE + name = 'optimize for ad hoc workloads' + AND value_in_use = 0 ) + HAVING COUNT(*) > 1; + END; + /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ + IF @@VERSION LIKE '%Microsoft SQL Server 2005%' + BEGIN + IF @CheckProcedureCacheFilter = 'CPU' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_worker_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - IF EXISTS ( SELECT 1 - FROM sys.configurations - WHERE name = 'remote admin connections' - AND value_in_use = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 100 ) - BEGIN + IF @CheckProcedureCacheFilter = 'Reads' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_logical_reads DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 100) WITH NOWAIT; + IF @CheckProcedureCacheFilter = 'ExecCount' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.execution_count DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 100 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Remote DAC Disabled' AS Finding , - 'https://BrentOzar.com/go/dac' AS URL , - 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.'; - END; + IF @CheckProcedureCacheFilter = 'Duration' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_elapsed_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; + END; + IF @ProductVersionMajor >= 10 + BEGIN + IF @CheckProcedureCacheFilter = 'CPU' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_worker_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - IF EXISTS ( SELECT * - FROM sys.dm_os_schedulers - WHERE is_online = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 101 ) - BEGIN + IF @CheckProcedureCacheFilter = 'Reads' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_logical_reads DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 101) WITH NOWAIT; + IF @CheckProcedureCacheFilter = 'ExecCount' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.execution_count DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 101 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'CPU Schedulers Offline' AS Finding , - 'https://BrentOzar.com/go/schedulers' AS URL , - 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.'; - END; + IF @CheckProcedureCacheFilter = 'Duration' + OR @CheckProcedureCacheFilter IS NULL + BEGIN + SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM sys.dm_exec_query_stats qs + ORDER BY qs.total_elapsed_time DESC) + INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) + SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] + FROM queries qs + LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset + WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; + EXECUTE(@StringToExecute); + END; + /* Populate the query_plan_filtered field. Only works in 2005SP2+, but we're just doing it in 2008 to be safe. */ + UPDATE #dm_exec_query_stats + SET query_plan_filtered = qp.query_plan + FROM #dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, + qs.statement_start_offset, + qs.statement_end_offset) + AS qp; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 110 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_os_memory_nodes') - BEGIN + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 110) WITH NOWAIT; + /* Populate the additional query_plan, text, and text_filtered fields */ + UPDATE #dm_exec_query_stats + SET query_plan = qp.query_plan , + [text] = st.[text] , + text_filtered = SUBSTRING(st.text, + ( qs.statement_start_offset + / 2 ) + 1, + ( ( CASE qs.statement_end_offset + WHEN -1 + THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END + - qs.statement_start_offset ) + / 2 ) + 1) + FROM #dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) + AS qp; - SET @StringToExecute = 'IF EXISTS (SELECT * - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - WHERE n.node_state_desc = ''OFFLINE'') - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 110 AS CheckID , - 50 AS Priority , - ''Performance'' AS FindingGroup , - ''Memory Nodes Offline'' AS Finding , - ''https://BrentOzar.com/go/schedulers'' AS URL , - ''Due to affinity masking or licensing problems, some of the memory may not be available.'' 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.'; - - EXECUTE(@StringToExecute); - END; + /* Dump instances of our own script. We're not trying to tune ourselves. */ + DELETE #dm_exec_query_stats + WHERE text LIKE '%sp_Blitz%' + OR text LIKE '%#BlitzResults%'; + /* Look for implicit conversions */ - IF EXISTS ( SELECT * - FROM sys.databases - WHERE state > 1 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 102 ) - BEGIN + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 63 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 63) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details , + QueryPlan , + QueryPlanFiltered + ) + SELECT 63 AS CheckID , + 120 AS Priority , + 'Query Plans' AS FindingsGroup , + 'Implicit Conversion' AS Finding , + 'https://www.brentozar.com/go/implicit' AS URL , + ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , + qs.query_plan , + qs.query_plan_filtered + FROM #dm_exec_query_stats qs + WHERE COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%CONVERT_IMPLICIT%' + AND COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%PhysicalOp="Index Scan"%'; + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 102) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 64 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 64) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details , + QueryPlan , + QueryPlanFiltered + ) + SELECT 64 AS CheckID , + 120 AS Priority , + 'Query Plans' AS FindingsGroup , + 'Implicit Conversion Affecting Cardinality' AS Finding , + 'https://www.brentozar.com/go/implicit' AS URL , + ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , + qs.query_plan , + qs.query_plan_filtered + FROM #dm_exec_query_stats qs + WHERE COALESCE(qs.query_plan_filtered, + CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= 10 + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 187 ) - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 107 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 107) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 107 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: ' + wait_type AS Finding , - 'https://BrentOzar.com/go/poison/#' + wait_type AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') - GROUP BY wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; + IF SERVERPROPERTY('IsHadrEnabled') = 1 + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 187) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT + 187 AS [CheckID] , + 230 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Endpoints Owned by Users' AS [Finding] , + 'https://www.brentozar.com/go/owners' AS [URL] , + ( 'Endpoint ' + ep.[name] + ' is owned by ' + SUSER_NAME(ep.principal_id) + '. If the endpoint owner login is disabled or not available due to Active Directory problems, the high availability will stop working.' + ) AS [Details] + FROM sys.database_mirroring_endpoints ep + LEFT OUTER JOIN sys.dm_server_services s ON SUSER_NAME(ep.principal_id) = s.service_account + WHERE s.service_account IS NULL AND ep.principal_id <> 1; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 121 ) + /*Verify that the servername is set */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 70 ) + BEGIN + IF @@SERVERNAME IS NULL BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 121) WITH NOWAIT; - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; + INSERT INTO #BlitzResults ( CheckID , Priority , @@ -5865,28 +8463,29 @@ AS URL , Details ) - SELECT 121 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: Serializable Locking' AS Finding , - 'https://BrentOzar.com/go/serializable' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; + SELECT 70 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + '@@Servername Not Set' AS Finding , + 'https://www.brentozar.com/go/servername' AS URL , + '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details; END; - - - - IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 162 ) - BEGIN + IF /* @@SERVERNAME IS set */ + (@@SERVERNAME IS NOT NULL + AND + /* not a named instance */ + CHARINDEX(CHAR(92),CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 + AND + /* not clustered, when computername may be different than the servername */ + SERVERPROPERTY('IsClustered') = 0 + AND + /* @@SERVERNAME is different than the computer name */ + @@SERVERNAME <> CAST(ISNULL(SERVERPROPERTY('ComputerNamePhysicalNetBIOS'),@@SERVERNAME) AS NVARCHAR(128)) ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; - INSERT INTO #BlitzResults ( CheckID , Priority , @@ -5895,11237 +8494,10886 @@ AS URL , Details ) - SELECT 162 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: CMEMTHREAD & NUMA' AS Finding , - 'https://BrentOzar.com/go/poison' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' - FROM sys.dm_os_nodes n - INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' - WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 - AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') - GROUP BY w.wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; + SELECT 70 AS CheckID , + 200 AS Priority , + 'Configuration' AS FindingsGroup , + '@@Servername Not Correct' AS Finding , + 'https://www.brentozar.com/go/servername' AS URL , + 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details; END; + END; + /*Check to see if a failsafe operator has been configured*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 73 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 73) WITH NOWAIT; + INSERT INTO #AlertInfo + EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 73 AS CheckID , + 200 AS Priority , + 'Monitoring' AS FindingsGroup , + 'No Failsafe Operator Configured' AS Finding , + 'https://www.brentozar.com/go/failsafe' AS URL , + ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details + FROM #AlertInfo + WHERE FailSafeOperator IS NULL; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 111 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 111) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - DatabaseName , - URL , - Details - ) - SELECT 111 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Possibly Broken Log Shipping' AS Finding , - d.[name] , - 'https://BrentOzar.com/go/shipping' AS URL , - d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' - FROM [master].sys.databases d - INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id - AND dm.mirroring_role IS NULL - WHERE ( d.[state] = 1 - OR (d.[state] = 0 AND d.[is_in_standby] = 1) ) - AND NOT EXISTS(SELECT * FROM msdb.dbo.restorehistory rh - INNER JOIN msdb.dbo.backupset bs ON rh.backup_set_id = bs.backup_set_id - WHERE d.[name] COLLATE SQL_Latin1_General_CP1_CI_AS = rh.destination_database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND rh.restore_date >= DATEADD(dd, -2, GETDATE())); - - END; + /*Identify globally enabled trace flags*/ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 74 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; + + INSERT INTO #TraceStatus + EXEC ( ' DBCC TRACESTATUS(-1) WITH NO_INFOMSGS' + ); + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Trace Flag On' AS Finding , + CASE WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN 'https://support.microsoft.com/en-us/kb/3210239' + WHEN [T].[TraceFlag] IN ('7745', '7752') THEN 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' + ELSE'https://www.BrentOzar.com/go/traceflags/' END AS URL , + 'Trace flag ' + + 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] = '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.' + WHEN [T].[TraceFlag] = '1204' THEN '1204 enabled globally, which captures deadlock graphs in the error log.' + WHEN [T].[TraceFlag] = '1222' THEN '1222 enabled globally, which captures deadlock graphs in the error log.' + WHEN [T].[TraceFlag] = '1224' THEN '1224 enabled globally, which disables lock escalation until the server has memory pressure. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '1806' THEN '1806 enabled globally, which disables Instant File Initialization, causing restores and file growths to take longer. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '2330' THEN '2330 enabled globally, which disables missing index requests. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '2371' THEN '2371 enabled globally, which changes the auto update stats threshold.' + WHEN [T].[TraceFlag] = '3023' THEN '3023 enabled globally, which performs checksums by default when doing database backups.' + 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 @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 @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?' + WHEN [T].[TraceFlag] = '8048' THEN '8048 enabled globally, which tries to reduce CMEMTHREAD waits on servers with a lot of logical processors.' + WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN '8017 is enabled globally, but this is the default for Express Edition.' + WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN '8017 is enabled globally, which disables the creation of schedulers for all logical processors.' + WHEN [T].[TraceFlag] = '8649' THEN '8649 enabled globally, which cost threshold for parallelism down to 0. This is usually a very bad idea.' + ELSE [T].[TraceFlag] + ' is enabled globally.' END + AS Details + FROM #TraceStatus T; IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 112 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'change_tracking_databases') + FROM #TraceStatus T + WHERE [T].[TraceFlag] = '7745' ) + AND @QueryStoreInUse = 1 + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + '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; + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 112) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #TraceStatus T + WHERE [T].[TraceFlag] = '7752' ) + AND @ProductVersionMajor < 15 + AND @QueryStoreInUse = 1 - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 112 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Change Tracking Enabled'' AS Finding, - ''https://BrentOzar.com/go/tracking'' AS URL, - ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id 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.'; - - EXECUTE(@StringToExecute); + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Recommended Trace Flag Off' AS Finding , + 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' AS URL , + 'Trace Flag 7752 not enabled globally. It stops queries needing to wait on Query Store loading up after database recovery. It is so recommended that it is enabled by default as of SQL Server 2019.' AS Details + FROM #TraceStatus T END; + END; + /* High CMEMTHREAD waits that could need trace flag 8048. + This check has to be run AFTER the globally enabled trace flag check, + since it uses the #TraceStatus table to know if flags are enabled. + */ + IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 162 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 162 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: CMEMTHREAD and NUMA' AS Finding , + 'https://www.brentozar.com/go/poison' AS URL , + CONVERT(VARCHAR(10), (MAX([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (MAX([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded' + + CASE WHEN ts.status = 1 THEN ' despite enabling trace flag 8048 already.' + ELSE '. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' + END + FROM sys.dm_os_nodes n + INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' + LEFT OUTER JOIN #TraceStatus ts ON ts.TraceFlag = 8048 AND ts.status = 1 + WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 + AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') + GROUP BY w.wait_type, ts.status + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + AND SUM([wait_time_ms]) > 60000; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 116 ) - AND EXISTS (SELECT * FROM msdb.sys.all_columns WHERE name = 'compressed_backup_size') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 116) WITH NOWAIT - SET @StringToExecute = 'INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 116 AS CheckID , - 200 AS Priority , - ''Informational'' AS FindingGroup , - ''Backup Compression Default Off'' AS Finding , - ''https://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.'' - 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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 + /*Check for transaction log file larger than data file */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 75 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 75) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 75 AS CheckID , + DB_NAME(a.database_id) , + 50 AS Priority , + 'Reliability' AS FindingsGroup , + 'Transaction Log Larger than Data File' AS Finding , + 'https://www.brentozar.com/go/biglog' AS URL , + 'The database [' + DB_NAME(a.database_id) + + '] has a ' + CAST((CAST(a.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(20)) + ' GB transaction log file, larger than the total data file sizes. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details + FROM sys.master_files a + WHERE a.type = 1 + AND DB_NAME(a.database_id) NOT IN ( + SELECT DISTINCT + DatabaseName FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 117 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_exec_query_resource_semaphores') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 117) WITH NOWAIT; - - SET @StringToExecute = 'IF 0 < (SELECT SUM([forced_grant_count]) FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL) - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 117 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Memory Pressure Affecting Queries'' AS Finding, - ''https://BrentOzar.com/go/grants'' AS URL, - CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' - FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT 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.'; - - EXECUTE(@StringToExecute); - END; - + WHERE CheckID = 75 OR CheckID IS NULL) + AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ + AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) + FROM sys.master_files b + WHERE a.database_id = b.database_id + AND b.type = 0 + ) + AND a.database_id IN ( + SELECT database_id + FROM sys.databases + WHERE source_database_id IS NULL ); + END; + /*Check for collation conflicts between user databases and tempdb */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 76 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 76) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 76 AS CheckID , + name AS DatabaseName , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Collation is ' + collation_name AS Finding , + 'https://www.brentozar.com/go/collate' AS URL , + 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details + FROM sys.databases + WHERE name NOT IN ( 'master', 'model', 'msdb') + AND name NOT LIKE 'ReportServer%' + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 76) + AND collation_name <> ( SELECT + collation_name + FROM + sys.databases + WHERE + name = 'tempdb' + ); + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 124 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 124) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 124, 150, 'Performance', 'Deadlocks Happening Daily', 'https://BrentOzar.com/go/deadlocks', - CAST(p.cntr_value AS NVARCHAR(100)) + ' deadlocks recorded since startup. To find them, run sp_BlitzLock.' AS Details - FROM sys.dm_os_performance_counters p - INNER JOIN sys.databases d ON d.name = 'tempdb' - WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' - AND RTRIM(p.instance_name) = '_Total' - AND p.cntr_value > 0 - AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 77 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 77) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 77 AS CheckID , + dSnap.[name] AS DatabaseName , + 170 AS Priority , + 'Reliability' AS FindingsGroup , + 'Database Snapshot Online' AS Finding , + 'https://www.brentozar.com/go/snapshot' AS URL , + 'Database [' + dSnap.[name] + + '] is a snapshot of [' + + dOriginal.[name] + + ']. Make sure you have enough drive space to maintain the snapshot as the original database grows.' AS Details + FROM sys.databases dSnap + INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id + AND dSnap.name NOT IN ( + SELECT DISTINCT DatabaseName + FROM #SkipChecks + WHERE CheckID = 77 OR CheckID IS NULL); + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 79 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 79) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 79 AS CheckID , + -- sp_Blitz Issue #776 + -- Job has history and was executed in the last 30 days OR Job is enabled AND Job Schedule is enabled + CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN + 100 + ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) + 200 + END AS Priority, + 'Performance' AS FindingsGroup , + 'Shrink Database Job' AS Finding , + 'https://www.brentozar.com/go/autoshrink' AS URL , + 'In the [' + j.[name] + '] job, step [' + + step.[step_name] + + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' + + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS Details + FROM msdb.dbo.sysjobs j + INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id + LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc + ON j.job_id = sjsc.job_id + LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc + ON sjsc.schedule_id = ssc.schedule_id + AND sjsc.job_id = j.job_id + LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh + ON j.job_id = sjh.job_id + AND step.step_id = sjh.step_id + AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date + AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time + WHERE step.command LIKE N'%SHRINKDATABASE%' + OR step.command LIKE N'%SHRINKFILE%'; + END; - IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 125 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 125) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://BrentOzar.com/askbrent/plan-cache-erased-recently/', - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' - FROM sys.dm_exec_query_stats WITH (NOLOCK) - ORDER BY creation_time; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 81 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 81) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 81 AS CheckID , + 200 AS Priority , + 'Non-Active Server Config' AS FindingsGroup , + cr.name AS Finding , + 'https://www.BrentOzar.com/blitz/sp_configure/' AS URL , + ( 'This sp_configure option isn''t running under its set value. Its set value is ' + + CAST(cr.[value] AS VARCHAR(100)) + + ' and its running value is ' + + CAST(cr.value_in_use AS VARCHAR(100)) + + '. When someone does a RECONFIGURE or restarts the instance, this setting will start taking effect.' ) AS Details + FROM sys.configurations cr + WHERE cr.value <> cr.value_in_use + AND NOT (cr.name = 'min server memory (MB)' AND cr.value IN (0,16) AND cr.value_in_use IN (0,16)); + END; - IF EXISTS (SELECT * FROM sys.configurations WHERE name = 'priority boost' AND (value = 1 OR value_in_use = 1)) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 126 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 126) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://BrentOzar.com/go/priorityboost/', - 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.'); - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 123 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 123) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 123 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Agent Jobs Starting Simultaneously' AS Finding , + 'https://www.brentozar.com/go/busyagent/' AS URL , + ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details + FROM msdb.dbo.sysjobactivity + WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) + GROUP BY start_execution_date HAVING COUNT(*) > 1; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 128 ) - BEGIN + IF @CheckServerInfo = 1 + BEGIN - IF (@ProductVersionMajor = 12 AND @ProductVersionMinor < 5000) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor < 6020) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 9 /*AND @ProductVersionMinor <= 5000*/) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 128) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://BrentOzar.com/go/unsupported', - 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + - CASE WHEN @ProductVersionMajor > 9 THEN - CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' - ELSE ' is no longer support by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); - END; +/*This checks Windows version. It would be better if Microsoft gave everything a separate build number, but whatever.*/ +IF @ProductVersionMajor >= 10 + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 172 ) + BEGIN + -- sys.dm_os_host_info includes both Windows and Linux info + IF EXISTS (SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' ) + BEGIN - END; - - /* Reliability - Dangerous Build of SQL Server (Corruption) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 129 ) - BEGIN - IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2342) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 129) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(129, 20, 'Reliability', 'Dangerous Build of SQL Server (Corruption)', 'http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) - END; + SELECT + 172 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Operating System Version' AS [Finding] , + ( CASE WHEN @IsWindowsOperatingSystem = 1 + THEN 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' + ELSE 'https://en.wikipedia.org/wiki/List_of_Linux_distributions' + END + ) AS [URL] , + ( CASE + WHEN [ohi].[host_platform] = 'Linux' THEN 'You''re running the ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ' distribution of ' + CAST([ohi].[host_platform] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' THEN 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + ELSE 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + END + ) AS [Details] + FROM [sys].[dm_os_host_info] [ohi]; + END; + ELSE + BEGIN + -- Otherwise, stick with Windows-only detection + + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_windows_info' ) - /* Reliability - Dangerous Build of SQL Server (Security) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 157 ) BEGIN - IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4000 AND @ProductVersionMinor <= 4017) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4251 AND @ProductVersionMinor <= 4319) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3129) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3300 AND @ProductVersionMinor <= 3447) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2253) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2300 AND @ProductVersionMinor <= 2370) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 157) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(157, 20, 'Reliability', 'Dangerous Build of SQL Server (Security)', 'https://technet.microsoft.com/en-us/library/security/MS14-044', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 172 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Windows Version' AS [Finding] , + 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' AS [URL] , + ( CASE + WHEN [owi].[windows_release] = '5' THEN 'You''re running Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running Windows Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] >= '6.2' AND [owi].[windows_release] <= '6.3' THEN 'You''re running Windows Server 2012/2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] = '10.0' THEN 'You''re running Windows Server 2016/2019 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + ELSE 'You''re running Windows Server, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + END + ) AS [Details] + FROM [sys].[dm_os_windows_info] [owi]; END; + END; + END; + +/* +This check hits the dm_os_process_memory system view +to see if locked_page_allocations_kb is > 0, +which could indicate that locked pages in memory is enabled. +*/ +IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 166 ) + BEGIN - /* Check if SQL 2016 Standard Edition but not SP1 */ + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + SELECT + 166 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Locked Pages In Memory Enabled' AS [Finding] , + 'https://www.brentozar.com/go/lpim' AS [URL] , + ( 'You currently have ' + + CASE WHEN [dopm].[locked_page_allocations_kb] / 1024. / 1024. > 0 + THEN CAST([dopm].[locked_page_allocations_kb] / 1024 / 1024 AS VARCHAR(100)) + + ' GB' + ELSE CAST([dopm].[locked_page_allocations_kb] / 1024 AS VARCHAR(100)) + + ' MB' + END + ' of pages locked in memory.' ) AS [Details] + FROM + [sys].[dm_os_process_memory] AS [dopm] + WHERE + [dopm].[locked_page_allocations_kb] > 0; + END; + + /* Server Info - Locked Pages In Memory Enabled - Check 166 - SQL Server 2016 SP1 and newer */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 189 ) + WHERE DatabaseName IS NULL AND CheckID = 166 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'sql_memory_model' ) BEGIN - IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 189) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(189, 100, 'Features', 'Missing Features', 'https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-server-2016-service-pack-1-sp1-released/', - 'SQL 2016 Standard Edition is being used but not Service Pack 1. Check the URL for a list of Enterprise Features that are included in Standard Edition as of SP1.'); - END; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 166 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Memory Model Unconventional'' AS Finding , + ''https://www.brentozar.com/go/lpim'' AS URL , + ''Memory Model: '' + CAST(sql_memory_model_desc AS NVARCHAR(100)) + FROM sys.dm_os_sys_info WHERE sql_memory_model <> 1 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.'; - END; + EXECUTE(@StringToExecute); + END; - /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 145 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 145) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 145 AS CheckID, - 10 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) - OR SUM(mem.pages_kb / 1024.0) > 250000 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.'; - - EXECUTE(@StringToExecute); - END; + /* Performance - Instant File Initialization Not Enabled - Check 192 */ + /* Server Info - Instant File Initialization Enabled - Check 193 */ + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 192 /* IFI disabled check disabled */ + ) OR NOT EXISTS + ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 193 /* IFI enabled check disabled */ + ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] and CheckId [%d].', 0, 1, 192, 193) WITH NOWAIT; + + DECLARE @IFISetting varchar(1) = N'N' + ,@IFIReadDMVFailed bit = 0 + ,@IFIAllFailed bit = 0; + + /* See if we can get the instant_file_initialization_enabled column from sys.dm_server_services */ + IF EXISTS + ( + SELECT 1/0 + FROM sys.all_columns + WHERE [object_id] = OBJECT_ID(N'[sys].[dm_server_services]') + AND [name] = N'instant_file_initialization_enabled' + ) + BEGIN + /* This needs to be a "dynamic" SQL statement because if the 'instant_file_initialization_enabled' column doesn't exist the procedure might fail on a bind error */ + SET @StringToExecute = N'SELECT @IFISetting = instant_file_initialization_enabled' + @crlf + + N'FROM sys.dm_server_services' + @crlf + + N'WHERE filename LIKE ''%sqlservr.exe%''' + @crlf + + N'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.'; + + EXEC dbo.sp_executesql + @StringToExecute + ,N'@IFISetting varchar(1) OUTPUT' + ,@IFISetting = @IFISetting OUTPUT + + SET @IFIReadDMVFailed = 0; + END + ELSE + /* We couldn't get the instant_file_initialization_enabled column from sys.dm_server_services, fall back to read error log */ + BEGIN + SET @IFIReadDMVFailed = 1; + /* If this is Amazon RDS, we'll use the rdsadmin.dbo.rds_read_error_log */ + IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND db_id('rdsadmin') IS NOT NULL + AND EXISTS ( SELECT 1/0 + FROM master.sys.all_objects + WHERE name IN ('rds_startup_tasks', 'rds_help_revlogin', 'rds_hexadecimal', 'rds_failover_tracking', 'rds_database_tracking', 'rds_track_change') + ) + BEGIN + /* Amazon RDS detected, read rdsadmin.dbo.rds_read_error_log */ + INSERT INTO #ErrorLog + EXEC rdsadmin.dbo.rds_read_error_log 0, 1, N'Database Instant File Initialization: enabled'; + END + ELSE + BEGIN + /* Try to read the error log, this might fail due to permissions */ + BEGIN TRY + INSERT INTO #ErrorLog + EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; + END TRY + BEGIN CATCH + IF @Debug IN (1, 2) RAISERROR('No permissions to execute xp_readerrorlog.', 0, 1) WITH NOWAIT; + SET @IFIAllFailed = 1; + END CATCH + END; + END; + + IF @IFIAllFailed = 0 + BEGIN + IF @IFIReadDMVFailed = 1 + /* We couldn't read the DMV so set the @IFISetting variable using the error log */ + BEGIN + IF EXISTS ( SELECT 1/0 + FROM #ErrorLog + WHERE LEFT([Text], 45) = N'Database Instant File Initialization: enabled' + ) + BEGIN + SET @IFISetting = 'Y'; + END + ELSE + BEGIN + SET @IFISetting = 'N'; + END; + END; + + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 192 /* IFI disabled check disabled */ + ) AND @IFISetting = 'N' + BEGIN + INSERT INTO #BlitzResults + ( + CheckID , + [Priority] , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 192 AS [CheckID] , + 50 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Instant File Initialization Not Enabled' AS [Finding] , + 'https://www.brentozar.com/go/instant' AS [URL] , + 'Consider enabling IFI for faster restores and data file growths.' AS [Details] + END; + + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 193 /* IFI enabled check disabled */ + ) AND @IFISetting = 'Y' + BEGIN + INSERT INTO #BlitzResults + ( + CheckID , + [Priority] , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 193 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Instant File Initialization Enabled' AS [Finding] , + 'https://www.brentozar.com/go/instant' AS [URL] , + 'The service account has the Perform Volume Maintenance Tasks permission.' AS [Details] + END; + END; + END; + /* End of checkId 192 */ + /* End of checkId 193 */ - /* Performance - In-Memory OLTP (Hekaton) In Use */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 146 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 146) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 146 AS CheckID, - 200 AS Priority, - ''Performance'' AS FindingsGroup, - ''In-Memory OLTP (Hekaton) In Use'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING SUM(mem.pages_kb / 1024.0) > 10 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.'; - - EXECUTE(@StringToExecute); - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 130 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 130) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 130 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Server Name' AS Finding , + 'https://www.brentozar.com/go/servername' AS URL , + @@SERVERNAME AS Details + WHERE @@SERVERNAME IS NOT NULL; + END; - /* In-Memory OLTP (Hekaton) - Transaction Errors */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 147 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_xtp_transaction_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 147) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 147 AS CheckID, - 100 AS Priority, - ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, - ''Transaction Errors'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details - FROM sys.dm_xtp_transaction_stats - WHERE validation_failures <> 0 - OR dependencies_failed <> 0 - OR write_conflicts <> 0 - OR unique_constraint_violations <> 0 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.'; - - EXECUTE(@StringToExecute); - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 83 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 83) WITH NOWAIT; + + -- DATETIMEOFFSET and DATETIME have different minimum values, so there's + -- a small workaround here to force 1753-01-01 if the minimum is detected + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 83 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Services'' AS Finding , + '''' AS URL , + 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; + IF @Debug = 2 AND @StringToExecute IS NULL PRINT '@StringToExecute has gone NULL, for some reason.'; + + EXECUTE(@StringToExecute); + END; + END; + /* Check 84 - SQL Server 2012 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 84 ) + BEGIN + IF EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'physical_memory_kb' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 84 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware'' AS Finding , + '''' AS URL , + ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_kb / 1024.0 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' + FROM sys.dm_os_sys_info 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.'; + + EXECUTE(@StringToExecute); + END; + /* Check 84 - SQL Server 2008 */ + IF EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'physical_memory_in_bytes' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 84 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware'' AS Finding , + '''' AS URL , + ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_in_bytes / 1024.0 / 1024 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' + FROM sys.dm_os_sys_info 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.'; + + EXECUTE(@StringToExecute); + END; + END; - /* Reliability - Database Files on Network File Shares */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 148 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 148) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 85 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 85) WITH NOWAIT; INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 148 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files on Network File Shares' AS Finding , - 'https://BrentOzar.com/go/nas' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE '\\%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 148); - END; + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 85 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'SQL Server Service' AS Finding , + '' AS URL , + N'Version: ' + + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) + + N'. Patch Level: ' + + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) + + CASE WHEN SERVERPROPERTY('ProductUpdateLevel') IS NULL + THEN N'' + ELSE N'. Cumulative Update: ' + + CAST(SERVERPROPERTY('ProductUpdateLevel') AS NVARCHAR(100)) + END + + N'. Edition: ' + + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + + N'. Availability Groups Enabled: ' + + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), + 0) AS VARCHAR(100)) + + N'. Availability Groups Manager Status: ' + + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), + 0) AS VARCHAR(100)); + END; - /* Reliability - Database Files Stored in Azure */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 149 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 149) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 88 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 88) WITH NOWAIT; INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 149 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files Stored in Azure' AS Finding , - 'https://BrentOzar.com/go/azurefiles' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE 'http://%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 149); - END; - + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 88 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'SQL Server Last Restart' AS Finding , + '' AS URL , + CAST(create_date AS VARCHAR(100)) + FROM sys.databases + WHERE database_id = 2; + END; - /* Reliability - Errors Logged Recently in the Default Trace */ - - /* First, let's check that there aren't any issues with the trace files */ - BEGIN TRY - - INSERT INTO #fnTraceGettable - ( TextData , - DatabaseName , - EventClass , - Severity , - StartTime , - EndTime , - Duration , - NTUserName , - NTDomainName , - HostName , - ApplicationName , - LoginName , - DBUserName - ) - SELECT TOP 20000 - CONVERT(NVARCHAR(4000),t.TextData) , - t.DatabaseName , - t.EventClass , - t.Severity , - t.StartTime , - t.EndTime , - t.Duration , - t.NTUserName , - t.NTDomainName , - t.HostName , - t.ApplicationName , - t.LoginName , - t.DBUserName - FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t - WHERE - ( - t.EventClass = 22 - AND t.Severity >= 17 - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - ) - OR - ( - t.EventClass IN (92, 93) - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - AND t.Duration > 15000000 - ) - OR - ( - t.EventClass IN (94, 95, 116) - ) - - SET @TraceFileIssue = 0 - - END TRY - BEGIN CATCH - - SET @TraceFileIssue = 1 - - END CATCH - - IF @TraceFileIssue = 1 - BEGIN IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 199 ) + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 91 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 91) WITH NOWAIT; INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - '199' AS CheckID , - '' AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'There Is An Error With The Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - 'Somebody has been messing with your trace files. Check the files are present at ' + @base_tracefilename AS Details - END - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 150 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 91 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Server Last Restart' AS Finding , + '' AS URL , + CAST(DATEADD(SECOND, (ms_ticks/1000)*(-1), GETDATE()) AS nvarchar(25)) + FROM sys.dm_os_sys_info; + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 150) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 92 ) + BEGIN - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 150 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Errors Logged Recently in the Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - CAST(t.TextData AS NVARCHAR(4000)) AS Details - FROM #fnTraceGettable t - WHERE t.EventClass = 22 - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.Severity >= 17 - --AND t.StartTime > DATEADD(dd, -30, GETDATE()); - END; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 92) WITH NOWAIT; + + INSERT INTO #driveInfo + ( drive, available_MB ) + EXEC master..xp_fixeddrives; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_volume_stats') + BEGIN + SET @StringToExecute = 'Update #driveInfo + SET + logical_volume_name = v.logical_volume_name, + total_MB = v.total_MB, + used_percent = v.used_percent + FROM + #driveInfo + inner join ( + SELECT DISTINCT + SUBSTRING(volume_mount_point, 1, 1) AS volume_mount_point + ,CASE WHEN ISNULL(logical_volume_name,'''') = '''' THEN '''' ELSE ''('' + logical_volume_name + '')'' END AS logical_volume_name + ,total_bytes/1024/1024 AS total_MB + ,available_bytes/1024/1024 AS available_MB + ,(CONVERT(DECIMAL(5,2),(total_bytes/1.0 - available_bytes)/total_bytes * 100)) AS used_percent + FROM + (SELECT TOP 1 WITH TIES + database_id + ,file_id + ,SUBSTRING(physical_name,1,1) AS Drive + FROM sys.master_files + ORDER BY ROW_NUMBER() OVER(PARTITION BY SUBSTRING(physical_name,1,1) ORDER BY database_id) + ) f + CROSS APPLY + sys.dm_os_volume_stats(f.database_id, f.file_id) + ) as v on #driveInfo.drive = v.volume_mount_point;'; + EXECUTE(@StringToExecute); + END; + SET @StringToExecute ='INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 92 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Drive '' + i.drive + '' Space'' AS Finding , + '''' AS URL , + CASE WHEN i.total_MB IS NULL THEN + CAST(CAST(i.available_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB free on '' + i.drive + + '' drive'' + ELSE CAST(CAST(i.available_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB free on '' + i.drive + + '' drive '' + i.logical_volume_name + + '' out of '' + CAST(CAST(i.total_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB total ('' + CAST(i.used_percent AS VARCHAR(30)) + ''% used)'' END + AS Details + FROM #driveInfo AS i;' + + IF (@ProductVersionMajor >= 11) + BEGIN + SET @StringToExecute=REPLACE(@StringToExecute,'CAST(i.available_MB/1024 AS NUMERIC(18,2))','FORMAT(i.available_MB/1024,''N2'')'); + SET @StringToExecute=REPLACE(@StringToExecute,'CAST(i.total_MB/1024 AS NUMERIC(18,2))','FORMAT(i.total_MB/1024,''N2'')'); + END; - /* Performance - File Growths Slow */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 151 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 151) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 151 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'File Growths Slow' AS Finding , - 'https://BrentOzar.com/go/filegrowth' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting file autogrowth to a smaller increment.' AS Details - FROM #fnTraceGettable t - WHERE t.EventClass IN (92, 93) - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.StartTime > DATEADD(dd, -30, GETDATE()) - --AND t.Duration > 15000000 - GROUP BY t.DatabaseName - HAVING COUNT(*) > 1; - END; + EXECUTE(@StringToExecute); + DROP TABLE #driveInfo; + END; - /* Performance - Many Plans for One Query */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 160 ) - AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 160) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 103 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'virtual_machine_type_desc' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 103) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 160 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Many Plans for One Query'' AS Finding, - ''https://BrentOzar.com/go/parameterization'' AS URL, - CAST(COUNT(DISTINCT plan_handle) AS NVARCHAR(50)) + '' plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa - WHERE pa.attribute = ''dbid'' - GROUP BY qs.query_hash, pa.value - HAVING COUNT(DISTINCT plan_handle) > 50 - ORDER BY COUNT(DISTINCT plan_handle) DESC OPTION (RECOMPILE);'; - + SELECT 103 AS CheckID, + 250 AS Priority, + ''Server Info'' AS FindingsGroup, + ''Virtual Server'' AS Finding, + ''https://www.brentozar.com/go/virtual'' AS URL, + ''Type: ('' + virtual_machine_type_desc + '')'' AS Details + FROM sys.dm_os_sys_info + WHERE virtual_machine_type <> 0 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.'; EXECUTE(@StringToExecute); - END; - + END; - /* Performance - High Number of Cached Plans */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 161 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 161) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 214 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'container_type_desc' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 214) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 161 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Number of Cached Plans'' AS Finding, - ''https://BrentOzar.com/go/planlimits'' AS URL, - ''Your server configuration is limited to '' + CAST(ht.buckets_count * 4 AS VARCHAR(20)) + '' '' + ht.name + '', and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details - FROM sys.dm_os_memory_cache_hash_tables ht - INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type - where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) - AND cc.entries_count >= (3 * ht.buckets_count) OPTION (RECOMPILE)'; - + SELECT 214 AS CheckID, + 250 AS Priority, + ''Server Info'' AS FindingsGroup, + ''Container'' AS Finding, + ''https://www.brentozar.com/go/virtual'' AS URL, + ''Type: ('' + container_type_desc + '')'' AS Details + FROM sys.dm_os_sys_info + WHERE container_type_desc <> ''NONE'' 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.'; EXECUTE(@StringToExecute); - END; - + END; - /* Performance - Too Much Free Memory */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 165 ) + WHERE DatabaseName IS NULL AND CheckID = 114 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + WHERE o.name = 'dm_os_memory_nodes' ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_nodes' + AND c.name = 'processor_group' ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 165) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 114) WITH NOWAIT; - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://BrentOzar.com/go/freememory', - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool, which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - END; - - - /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 155 ) - AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 155) WITH NOWAIT; + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 114 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Hardware - NUMA Config'' AS Finding , + '''' AS URL , + ''Node: '' + CAST(n.node_id AS NVARCHAR(10)) + '' State: '' + node_state_desc + + '' Online schedulers: '' + CAST(n.online_scheduler_count AS NVARCHAR(10)) + '' Offline schedulers: '' + CAST(oac.offline_schedulers AS VARCHAR(100)) + '' Processor Group: '' + CAST(n.processor_group AS NVARCHAR(10)) + + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) + FROM sys.dm_os_nodes n + INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id + OUTER APPLY (SELECT + COUNT(*) AS [offline_schedulers] + FROM sys.dm_os_schedulers dos + WHERE n.node_id = dos.parent_node_id + AND dos.status = ''VISIBLE OFFLINE'' + ) oac + WHERE n.node_state_desc NOT LIKE ''%DAC%'' + ORDER BY n.node_id 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.'; + + EXECUTE(@StringToExecute); + END; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 155 AS CheckID , - 0 AS Priority , - 'Outdated sp_Blitz' AS FindingsGroup , - 'sp_Blitz is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details; - END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 211 ) + BEGIN + + /* Variables for check 211: */ + DECLARE + @powerScheme varchar(36) + ,@cpu_speed_mhz int + ,@cpu_speed_ghz decimal(18,2) + ,@ExecResult int; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 211) WITH NOWAIT; + IF @sa = 0 RAISERROR('The errors: ''xp_regread() returned error 5, ''Access is denied.'''' can be safely ignored', 0, 1) WITH NOWAIT; + + /* Get power plan if set by group policy [Git Hub Issue #1620] */ + EXEC xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'SOFTWARE\Policies\Microsoft\Power\PowerSettings', + @value_name = N'ActivePowerScheme', + @value = @powerScheme OUTPUT, + @no_output = N'no_output'; + + IF @powerScheme IS NULL /* If power plan was not set by group policy, get local value [Git Hub Issue #1620]*/ + EXEC xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes', + @value_name = N'ActivePowerScheme', + @value = @powerScheme OUTPUT; + + /* Get the cpu speed*/ + EXEC @ExecResult = xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'HARDWARE\DESCRIPTION\System\CentralProcessor\0', + @value_name = N'~MHz', + @value = @cpu_speed_mhz OUTPUT; - /* Populate a list of database defaults. I'm doing this kind of oddly - - it reads like a lot of work, but this way it compiles & runs on all - versions of SQL Server. - */ - - IF @Debug IN (1, 2) RAISERROR('Generating database defaults.', 0, 1) WITH NOWAIT; - - INSERT INTO #DatabaseDefaults - SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_read_committed_snapshot_on', 0, 133, 210, 'Read Committed Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); - /* Not alerting for this since we actually want it and we have a separate check for it: - INSERT INTO #DatabaseDefaults - SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); - */ - INSERT INTO #DatabaseDefaults - SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); + /* Convert the Megahertz to Gigahertz */ + IF @ExecResult != 0 RAISERROR('We couldn''t retrieve the CPU speed, you will see Unknown in the results', 0, 1) - DECLARE DatabaseDefaultsLoop CURSOR FOR - SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details - FROM #DatabaseDefaults; + SET @cpu_speed_ghz = CAST(CAST(@cpu_speed_mhz AS decimal) / 1000 AS decimal(18,2)); - OPEN DatabaseDefaultsLoop; - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - WHILE @@FETCH_STATUS = 0 - BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 211 AS CheckId, + 250 AS Priority, + 'Server Info' AS FindingsGroup, + 'Power Plan' AS Finding, + 'https://www.brentozar.com/blitz/power-mode/' AS URL, + 'Your server has ' + + ISNULL(CAST(@cpu_speed_ghz as VARCHAR(8)), 'Unknown ') + + 'GHz CPUs, and is in ' + + CASE @powerScheme + WHEN 'a1841308-3541-4fab-bc81-f71556f20b4a' + THEN 'power saving mode -- are you sure this is a production SQL Server?' + WHEN '381b4222-f694-41f0-9685-ff5bb260df2e' + THEN 'balanced power mode -- Uh... you want your CPUs to run at full speed, right?' + WHEN '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' + THEN 'high performance power mode' + WHEN 'e9a42b02-d5df-448d-aa00-03f14749eb61' + THEN 'ultimate performance power mode' + ELSE 'an unknown power mode.' + END AS Details + + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, @CurrentCheckID) WITH NOWAIT; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 212 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 212) WITH NOWAIT; - /* Target Recovery Time (142) can be either 0 or 60 due to a number of bugs */ - IF @CurrentCheckID = 142 - 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.[' + @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.[' + @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.'; - - EXEC (@StringToExecute); + INSERT INTO #Instances (Instance_Number, Instance_Name, Data_Field) + EXEC master.sys.xp_regread @rootkey = 'HKEY_LOCAL_MACHINE', + @key = 'SOFTWARE\Microsoft\Microsoft SQL Server', + @value_name = 'InstalledInstances' + + IF (SELECT COUNT(*) FROM #Instances) > 1 + BEGIN - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - END; + DECLARE @InstanceCount NVARCHAR(MAX) + SELECT @InstanceCount = COUNT(*) FROM #Instances - CLOSE DatabaseDefaultsLoop; - DEALLOCATE DatabaseDefaultsLoop; + INSERT INTO #BlitzResults + ( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 212 AS CheckId , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Instance Stacking' AS Finding , + 'https://www.brentozar.com/go/babygotstacked/' AS URL , + 'Your Server has ' + @InstanceCount + ' Instances of SQL Server installed. More than one is usually a bad idea. Read the URL for more info.' + END; + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 106 ) + AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 + AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') + AND @TraceFileIssue = 0 + BEGIN -/*This checks to see if Agent is Offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 167 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 167) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 106) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 106 AS CheckID + ,250 AS Priority + ,'Server Info' AS FindingsGroup + ,'Default Trace Contents' AS Finding + ,'https://www.brentozar.com/go/trace' AS URL + ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as VARCHAR(30))+' hours of data' + +' between '+cast(Min(StartTime) as VARCHAR(30))+' and '+cast(GETDATE()as VARCHAR(30)) + +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + ) as Details + FROM ::fn_trace_gettable( @base_tracefilename, default ) + WHERE EventClass BETWEEN 65500 and 65600; + END; /* CheckID 106 */ - SELECT - 167 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Agent is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Server Agent%' - AND CAST(SERVERPROPERTY('Edition') AS VARCHAR(1000)) NOT LIKE '%xpress%'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 152 ) + BEGIN + IF EXISTS (SELECT * FROM sys.dm_os_wait_stats ws + LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type + WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 + AND i.wait_type IS NULL) + BEGIN + /* Check for waits that have had more than 10% of the server's wait time */ + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 152) WITH NOWAIT; + + WITH os(wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) + AS + (SELECT ws.wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms + FROM sys.dm_os_wait_stats ws + LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type + WHERE i.wait_type IS NULL + AND wait_time_ms > .1 * @CpuMsSinceWaitsCleared + AND waiting_tasks_count > 0) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 9 + 152 AS CheckID + ,240 AS Priority + ,'Wait Stats' AS FindingsGroup + , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding + ,'https://www.sqlskills.com/help/waits/' + LOWER(os.wait_type) + '/' AS URL + , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' hours of waits, ' + + CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + + /* CAST(CAST( + 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.wait_time_ms) OVER () ) + AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% of waits, ' + */ + CAST(CAST( + 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.wait_time_ms) OVER ()) + AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + + CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + + CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 + THEN + CAST( + SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) + / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) + AS NUMERIC(18,1)) + ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' + FROM os + ORDER BY SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) DESC; + END; /* IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND waiting_tasks_count > 0) */ - END; - END; + /* If no waits were found, add a note about that */ + IF NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (107, 108, 109, 121, 152, 162)) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 153) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://www.brentozar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); + END; + END; /* CheckID 152 */ -/*This checks to see if the Full Text thingy is offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 168 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + /* CheckID 222 - Server Info - Azure Managed Instance */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 222 ) + AND 4 = ( SELECT COUNT(*) + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_job_object' + AND c.name IN ('cpu_rate', 'memory_limit_mb', 'process_memory_limit_mb', 'workingset_limit_mb' )) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 222) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 222 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Azure Managed Instance'' AS Finding , + ''https://www.BrentOzar.com/go/azurevm'' AS URL , + ''cpu_rate: '' + CAST(COALESCE(cpu_rate, 0) AS VARCHAR(20)) + + '', memory_limit_mb: '' + CAST(COALESCE(memory_limit_mb, 0) AS NVARCHAR(20)) + + '', process_memory_limit_mb: '' + CAST(COALESCE(process_memory_limit_mb, 0) AS NVARCHAR(20)) + + '', workingset_limit_mb: '' + CAST(COALESCE(workingset_limit_mb, 0) AS NVARCHAR(20)) + FROM sys.dm_os_job_object 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.'; + + EXECUTE(@StringToExecute); + END; - SELECT - 168 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; + /* CheckID 224 - Performance - SSRS/SSAS/SSIS Installed */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 224 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 224) WITH NOWAIT; + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + BEGIN + + IF OBJECT_ID('tempdb..#services') IS NOT NULL DROP TABLE #services; + CREATE TABLE #services (cmdshell_output varchar(max)); + + INSERT INTO #services + EXEC /**/xp_cmdshell/**/ 'net start' /* added comments around command since some firewalls block this string TL 20210221 */ + + IF EXISTS (SELECT 1 + FROM #services + WHERE cmdshell_output LIKE '%SQL Server Reporting Services%' + OR cmdshell_output LIKE '%SQL Server Integration Services%' + OR cmdshell_output LIKE '%SQL Server Analysis Services%') + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 224 AS CheckID + ,200 AS Priority + ,'Performance' AS FindingsGroup + ,'SSAS/SSIS/SSRS Installed' AS Finding + ,'https://www.BrentOzar.com/go/services' AS URL + ,'Did you know you have other SQL Server services installed on this box other than the engine? It can be a real performance pain.' as Details + + END; + + END; + END; - END; - END; + /* CheckID 232 - Server Info - Data Size */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 232 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 232) WITH NOWAIT; + + IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL + DROP TABLE #MasterFiles; + CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); + /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ + IF ((SERVERPROPERTY('Edition')) = 'SQL Azure' + AND (OBJECT_ID('sys.master_files') IS NULL)) + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;'; + ELSE + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;'; + EXEC(@StringToExecute); -/*This checks which service account SQL Server is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 169 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 169) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 232 AS CheckID + ,250 AS Priority + ,'Server Info' AS FindingsGroup + ,'Data Size' AS Finding + ,'' AS URL + ,CAST(COUNT(DISTINCT database_id) AS NVARCHAR(100)) + N' databases, ' + CAST(CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS MONEY) AS VARCHAR(100)) + ' GB total file size' as Details + FROM #MasterFiles + WHERE database_id > 4; - SELECT - 169 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server%' - AND [servicename] NOT LIKE 'SQL Server Agent%'; + END; - END; - END; + /* CheckID 260 - Security - SQL Server service account is member of Administrators */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 260 ) AND @ProductVersionMajor >= 10 + BEGIN + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + AND EXISTS ( SELECT 1 FROM sys.all_objects WHERE [name] = 'dm_server_services' ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 260) WITH NOWAIT; + IF OBJECT_ID('tempdb..#localadmins') IS NOT NULL DROP TABLE #localadmins; + CREATE TABLE #localadmins (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 */ + + IF EXISTS (SELECT 1 + FROM #localadmins + 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 Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 260 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server''s service account is a member of the local Administrators group - meaning that anyone who can use xp_cmdshell can do anything on the host.' as Details + + END; + + END; + END; -/*This checks which service account SQL Agent is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 170 ) + /* CheckID 261 - Security - SQL Server Agent service account is member of Administrators */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 261 ) AND @ProductVersionMajor >= 10 + BEGIN + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + AND EXISTS ( SELECT 1 FROM sys.all_objects WHERE [name] = 'dm_server_services' ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 261) WITH NOWAIT; + /*If this table exists and CheckId 260 was not skipped, then we're piggybacking off of 260's results */ + IF OBJECT_ID('tempdb..#localadmins') IS NOT NULL + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 260 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('CheckId [%d] - found #localadmins table from CheckID 260 - no need to call xp_cmdshell again', 0, 1, 261) WITH NOWAIT; + + IF EXISTS (SELECT 1 + FROM #localadmins + WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) + FROM [sys].[dm_server_services] + WHERE [servicename] LIKE 'SQL Server%Agent%' + AND [servicename] NOT LIKE 'SQL Server Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 261 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server Agent''s service account is a member of the local Administrators group - meaning that anyone who can create and run jobs can do anything on the host.' as Details + + END; + END; /*piggyback*/ + ELSE /*can't piggyback*/ + BEGIN + /*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)); + /* 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 + WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) + FROM [sys].[dm_server_services] + WHERE [servicename] LIKE 'SQL Server%Agent%' + AND [servicename] NOT LIKE 'SQL Server Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 261 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server Agent''s service account is a member of the local Administrators group - meaning that anyone who can create and run jobs can do anything on the host.' as Details + + END; + + END;/*can't piggyback*/ + END; + END; /* CheckID 261 */ - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 170) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 170 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server Agent is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server Agent%'; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 266 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 266 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Hardware - Memory Counters' AS Finding , + 'https://www.brentozar.com/go/target' AS URL , + N'Target Server Memory (GB): ' + CAST((CAST((pTarget.cntr_value / 1024.0 / 1024.0) AS DECIMAL(10,1))) AS NVARCHAR(100)) + + N' Total Server Memory (GB): ' + CAST((CAST((pTotal.cntr_value / 1024.0 / 1024.0) AS DECIMAL(10,1))) AS NVARCHAR(100)) + FROM sys.dm_os_performance_counters pTarget + INNER JOIN sys.dm_os_performance_counters pTotal + ON pTotal.object_name LIKE 'SQLServer:Memory Manager%' + AND pTotal.counter_name LIKE 'Total Server Memory (KB)%' + WHERE pTarget.object_name LIKE 'SQLServer:Memory Manager%' + AND pTarget.counter_name LIKE 'Target Server Memory (KB)%' + END - END; - END; -/*This counts memory dumps and gives min and max date of in view*/ -IF @ProductVersionMajor >= 10 - AND NOT (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 4297) /* Skip due to crash bug: https://support.microsoft.com/en-us/help/2908087 */ - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 171 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_memory_dumps' ) - BEGIN - IF 5 <= (SELECT COUNT(*) FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 171) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + END; /* IF @CheckServerInfo = 1 */ + END; /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ - SELECT - 171 AS [CheckID] , - 20 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Memory Dumps Have Occurred' AS [Finding] , - 'https://BrentOzar.com/go/dump' AS [URL] , - ( 'That ain''t good. I''ve had ' + - CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + - CAST(CAST(MIN([creation_time]) AS DATETIME) AS VARCHAR(100)) + - ' and ' + - CAST(CAST(MAX([creation_time]) AS DATETIME) AS VARCHAR(100)) + - '!' - ) AS [Details] - FROM - [sys].[dm_server_memory_dumps] - WHERE [creation_time] >= DATEADD(year, -1, GETDATE()); + /* Delete priorites they wanted to skip. */ + IF @IgnorePrioritiesAbove IS NOT NULL + DELETE #BlitzResults + WHERE [Priority] > @IgnorePrioritiesAbove AND CheckID <> -1; - END; - END; + IF @IgnorePrioritiesBelow IS NOT NULL + DELETE #BlitzResults + WHERE [Priority] < @IgnorePrioritiesBelow AND CheckID <> -1; + + /* Delete checks they wanted to skip. */ + IF @SkipChecksTable IS NOT NULL + BEGIN + DELETE FROM #BlitzResults + WHERE DatabaseName IN ( SELECT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); + DELETE FROM #BlitzResults + WHERE CheckID IN ( SELECT CheckID + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); + DELETE r FROM #BlitzResults r + INNER JOIN #SkipChecks c ON r.DatabaseName = c.DatabaseName and r.CheckID = c.CheckID + AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName')); END; -/*Checks to see if you're on Developer or Evaluation*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 173 ) + /* Add summary mode */ + IF @SummaryMode > 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 173) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + UPDATE #BlitzResults + SET Finding = br.Finding + ' (' + CAST(brTotals.recs AS NVARCHAR(20)) + ')' + FROM #BlitzResults br + INNER JOIN (SELECT FindingsGroup, Finding, Priority, COUNT(*) AS recs FROM #BlitzResults GROUP BY FindingsGroup, Finding, Priority) brTotals ON br.FindingsGroup = brTotals.FindingsGroup AND br.Finding = brTotals.Finding AND br.Priority = brTotals.Priority + WHERE brTotals.recs > 1; - SELECT - 173 AS [CheckID] , - 200 AS [Priority] , - 'Licensing' AS [FindingsGroup] , - 'Non-Production License' AS [Finding] , - 'https://BrentOzar.com/go/licensing' AS [URL] , - ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + - CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + - ' the good folks at Microsoft might get upset with you. Better start counting those cores.' - ) AS [Details] - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Developer%' - OR CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Evaluation%'; + DELETE br + FROM #BlitzResults br + WHERE EXISTS (SELECT * FROM #BlitzResults brLower WHERE br.FindingsGroup = brLower.FindingsGroup AND br.Finding = brLower.Finding AND br.Priority = brLower.Priority AND br.ID > brLower.ID); END; -/*Checks to see if Buffer Pool Extensions are in use*/ - IF @ProductVersionMajor >= 12 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 174 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 174) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 174 AS [CheckID] , - 200 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Buffer Pool Extensions Enabled' AS [Finding] , - 'https://BrentOzar.com/go/bpe' AS [URL] , - ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + - [path] + - '. It''s currently ' + - CASE WHEN [current_size_in_kb] / 1024. / 1024. > 0 - THEN CAST([current_size_in_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([current_size_in_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + - '. Did you know that BPEs only provide single threaded access 8KB (one page) at a time?' - ) AS [Details] - FROM sys.dm_os_buffer_pool_extension_configuration - WHERE [state_description] <> 'BUFFER POOL EXTENSION DISABLED'; - - END; + /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 255 , + 'Thanks!' , + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org' , + 'We hope you found this tool useful.' + ); -/*Check for too many tempdb files*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 175 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 175) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 175 AS CheckID , - 'TempDB' AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB Has >16 Data Files' AS Finding , - 'https://BrentOzar.com/go/tempdb' AS URL , - 'Woah, Nelly! TempDB has ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + '. Did you forget to terminate a loop somewhere?' AS Details - FROM sys.[master_files] AS [mf] - WHERE [mf].[database_id] = 2 AND [mf].[type] = 0 - HAVING COUNT_BIG(*) > 16; - END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 176 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_xe_sessions' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 176) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 176 AS CheckID , - '' AS DatabaseName , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Extended Events Hyperextension' AS Finding , - 'https://BrentOzar.com/go/xe' AS URL , - 'Hey big spender, you have ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + ' Extended Events sessions running. You sure you meant to do that?' AS Details - FROM sys.dm_xe_sessions - WHERE [name] NOT IN - ( 'AlwaysOn_health', 'system_health', 'telemetry_xevents', 'sp_server_diagnostics', 'hkenginexesession' ) - AND name NOT LIKE '%$A%' - HAVING COUNT_BIG(*) >= 2; - END; - END; - - /*Harmful startup parameter*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 177 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_registry' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 177) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 177 AS CheckID , - '' AS DatabaseName , - 5 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Disabled Internal Monitoring Features' AS Finding , - 'https://msdn.microsoft.com/en-us/library/ms190737.aspx' AS URL , - 'You have -x as a startup parameter. You should head to the URL and read more about what it does to your system.' AS Details - FROM - [sys].[dm_server_registry] AS [dsr] - WHERE - [dsr].[registry_key] LIKE N'%MSSQLServer\Parameters' - AND [dsr].[value_data] = '-x';; - END; - END; - - - /* Reliability - Dangerous Third Party Modules - 179 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 179 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 179) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + ) + VALUES ( -1 , + 0 , + 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), + 'SQL Server First Responder Kit' , + 'http://FirstResponderKit.org/' , + 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - SELECT - 179 AS [CheckID] , - 5 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Dangerous Third Party Modules' AS [Finding] , - 'https://support.microsoft.com/en-us/kb/2033238' AS [URL] , - ( COALESCE(company, '') + ' - ' + COALESCE(description, '') + ' - ' + COALESCE(name, '') + ' - suspected dangerous third party module is installed.') AS [Details] - FROM sys.dm_os_loaded_modules - WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') /* McAfee VirusScan Enterprise */ - OR UPPER(name) LIKE UPPER('%\HIPI.DLL') OR UPPER(name) LIKE UPPER('%\HcSQL.dll') OR UPPER(name) LIKE UPPER('%\HcApi.dll') OR UPPER(name) LIKE UPPER('%\HcThe.dll') /* McAfee Host Intrusion */ - OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') /* Sophos AV */ - OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL'); /* OSISoft PI data access */ + ); - END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details - /*Find shrink database tasks*/ + ) + SELECT 156 , + 254 , + 'Rundate' , + GETDATE() , + 'http://FirstResponderKit.org/' , + 'Captain''s log: stardate something and something...'; + + IF @EmailRecipients IS NOT NULL + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Sending an email.', 0, 1) WITH NOWAIT; + + /* Database mail won't work off a local temp table. I'm not happy about this hacky workaround either. */ + IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; + SELECT * INTO ##BlitzResults FROM #BlitzResults; + SET @query_result_separator = char(9); + SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details; SET NOCOUNT OFF;'; + SET @EmailSubject = 'sp_Blitz Results for ' + @@SERVERNAME; + SET @EmailBody = 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. http://FirstResponderKit.org'; + IF @EmailProfile IS NULL + EXEC msdb.dbo.sp_send_dbmail + @recipients = @EmailRecipients, + @subject = @EmailSubject, + @body = @EmailBody, + @query_attachment_filename = 'sp_Blitz-Results.csv', + @attach_query_result_as_file = 1, + @query_result_header = 1, + @query_result_width = 32767, + @append_query_error = 1, + @query_result_no_padding = 1, + @query_result_separator = @query_result_separator, + @query = @StringToExecute; + ELSE + EXEC msdb.dbo.sp_send_dbmail + @profile_name = @EmailProfile, + @recipients = @EmailRecipients, + @subject = @EmailSubject, + @body = @EmailBody, + @query_attachment_filename = 'sp_Blitz-Results.csv', + @attach_query_result_as_file = 1, + @query_result_header = 1, + @query_result_width = 32767, + @append_query_error = 1, + @query_result_no_padding = 1, + @query_result_separator = @query_result_separator, + @query = @StringToExecute; + IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 180 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ + /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ + DECLARE @ValidOutputServer BIT; + DECLARE @ValidOutputLocation BIT; + DECLARE @LinkedServerDBCheck NVARCHAR(2000); + DECLARE @ValidLinkedServerDB INT; + DECLARE @tmpdbchk table (cnt int); + IF @OutputServerName IS NOT NULL BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 180) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , [id] -- ID required to link maintenace plan with jobs and jobhistory (sp_Blitz Issue #776) - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ) - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 180 AS [CheckID] , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS [FindingsGroup] , - 'Shrink Database Step In Maintenance Plan' AS [Finding] , - 'https://BrentOzar.com/go/autoshrink' AS [URL] , - 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS [Details] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - join msdb.dbo.sysmaintplan_subplans as sms - on mps.id = sms.plan_id - JOIN msdb.dbo.sysjobs j - on sms.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobsteps AS step - ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE [c].[value]('(@dts:ObjectName)', 'VARCHAR(128)') = 'Shrink Database Task'; - - END; - - - /*Find repetitive maintenance tasks*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 181 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 181) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ), [maintenance_plan_table] AS ( - SELECT [mps].[name] - ,[c].[value]('(@dts:ObjectName)', 'NVARCHAR(128)') AS [step_name] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - ), [mp_steps_pretty] AS (SELECT DISTINCT [m1].[name] , - STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] - FOR XML PATH(N'')), 1, 2, N'') AS [maintenance_plan_steps] - FROM [maintenance_plan_table] AS [m1]) - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; - SELECT - 181 AS [CheckID] , - 100 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Repetitive Steps In Maintenance Plans' AS [Finding] , - 'https://ola.hallengren.com/' AS [URL] , - 'The maintenance plan ' + [m].[name] + ' is doing repetitive work on indexes and statistics. Perhaps it''s time to try something more modern?' AS [Details] - FROM [mp_steps_pretty] m - WHERE m.[maintenance_plan_steps] LIKE '%Rebuild%Reorganize%' - OR m.[maintenance_plan_steps] LIKE '%Rebuild%Update%'; + IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; + ELSE + BEGIN + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; - END; - + /* @OutputTableName lets us export the results to a permanent table */ + IF @ValidOutputLocation = 1 + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + DatabaseName NVARCHAR(128), + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL, + CheckID INT , + CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + IF @OutputXMLasNVARCHAR = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); + END; + EXEC(@StringToExecute); + END; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputServerName + '.' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputServerName + '.' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; - /* Reliability - No Failover Cluster Nodes Available - 184 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '10%' - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '9%' + EXEC(@StringToExecute); + END; + ELSE + BEGIN + IF @OutputXMLasNVARCHAR = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; + END; + ELSE + begin + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; + END; + EXEC(@StringToExecute); + + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 - 184 AS CheckID , - 20 AS Priority , - ''Reliability'' AS FindingsGroup , - ''No Failover Cluster Nodes Available'' AS Finding , - ''https://BrentOzar.com/go/node'' AS URL , - ''There are no failover cluster nodes available if the active node fails'' AS Details - FROM ( - SELECT SUM(CASE WHEN [status] = 0 AND [is_current_owner] = 0 THEN 1 ELSE 0 END) AS [available_nodes] - FROM sys.dm_os_cluster_nodes - ) a - WHERE [available_nodes] < 1 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.'; - - EXECUTE(@StringToExecute); + IF @ValidOutputServer = 1 + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' + + 'CREATE TABLE ' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + Priority TINYINT , + FindingsGroup VARCHAR(50) , + Finding VARCHAR(200) , + DatabaseName NVARCHAR(128), + URL VARCHAR(200) , + Details NVARCHAR(4000) , + QueryPlan [XML] NULL , + QueryPlanFiltered [NVARCHAR](MAX) NULL, + CheckID INT , + CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; + + EXEC(@StringToExecute); + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); END; - /* Reliability - TempDB File Error */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 191 ) - AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> (SELECT COUNT(*) FROM tempdb.sys.database_files) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 191) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 191 AS [CheckID] , - 50 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'TempDB File Error' AS [Finding] , - 'https://BrentOzar.com/go/tempdboops' AS [URL] , - 'Mismatch between the number of TempDB files in sys.master_files versus tempdb.sys.database_files' AS [Details]; - END; - -/*Perf - Odd number of cores in a socket*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID = 198 ) - AND EXISTS ( SELECT 1 - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 198) WITH NOWAIT - - INSERT INTO #BlitzResults - ( - CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details - ) - SELECT 198 AS CheckID, - NULL AS DatabaseName, - 10 AS Priority, - 'Performance' AS FindingsGroup, - 'CPU w/Odd Number of Cores' AS Finding, - 'https://BrentOzar.com/go/oddity' AS URL, - 'Node ' + CONVERT(VARCHAR(10), parent_node_id) + ' has ' + CONVERT(VARCHAR(10), COUNT(cpu_id)) - + CASE WHEN COUNT(cpu_id) = 1 THEN ' core assigned to it. This is a really bad NUMA configuration.' - ELSE ' cores assigned to it. This is a really bad NUMA configuration.' - END AS Details - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; - - END; - -/*Begin: checking default trace for odd DBCC activity*/ - - --Grab relevant event data - IF @TraceFileIssue = 0 - BEGIN - SELECT UPPER( - REPLACE( - SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, - ISNULL( - NULLIF( - CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), - 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. - , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ) - , '') --This replaces any optional WITH clause to a DBCC command, like tableresults. - ) AS [dbcc_event_trunc_upper], - UPPER( - REPLACE( - CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ), '')) AS [dbcc_event_full_upper], - MIN(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS min_start_time, - MAX(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS max_start_time, - t.NTUserName AS [nt_user_name], - t.NTDomainName AS [nt_domain_name], - t.HostName AS [host_name], - t.ApplicationName AS [application_name], - t.LoginName [login_name], - t.DBUserName AS [db_user_name] - INTO #dbcc_events_from_trace - FROM #fnTraceGettable AS t - WHERE t.EventClass = 116 - OPTION(RECOMPILE) - END; - - /*Overall count of DBCC events excluding silly stuff*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 203 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 199) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 203 AS CheckID , - 50 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'Overall Events' AS Finding , - '' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This does not include CHECKDB and other usually benign DBCC events.' - AS Details - FROM #dbcc_events_from_trace d - /* This WHERE clause below looks horrible, but it's because users can run stuff like - DBCC LOGINFO - with lots of spaces (or carriage returns, or comments) in between the DBCC and the - command they're trying to run. See Github issues 1062, 1074, 1075. - */ - WHERE d.dbcc_event_full_upper NOT LIKE '%DBCC%ADDINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKALLOC%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCATALOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCONSTRAINTS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKDB%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKFILEGROUP%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKIDENT%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKPRIMARYFILE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CLEANTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%DBINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%ERRORLOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INCREMENTINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INPUTBUFFER%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%LOGINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%OPENTRAN%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SETINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOWFILESTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOW_STATISTICS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%NETSTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%LOGSPACE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEON%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEOFF%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACESTATUS%' - AND d.application_name NOT LIKE 'Critical Care(R) Collector' - AND d.application_name NOT LIKE '%Red Gate Software Ltd SQL Prompt%' - AND d.application_name NOT LIKE '%Spotlight Diagnostic Server%' - AND d.application_name NOT LIKE '%SQL Diagnostic Manager%' - AND d.application_name NOT LIKE '%Sentry%' - - - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running drop clean buffers*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 207 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 200) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 207 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC DROPCLEANBUFFERS Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. If this is a production box, know that you''re clearing all data out of memory when this happens. What kind of monster would do that?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC DROPCLEANBUFFERS' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running free proc cache*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 208 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 201) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 208 AS CheckID , - 10 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'DBCC FREEPROCCACHE Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This has bad idea jeans written all over its butt, like most other bad idea jeans.' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC FREEPROCCACHE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; + DECLARE @separator AS VARCHAR(1); + IF @OutputType = 'RSV' + SET @separator = CHAR(31); + ELSE + SET @separator = ','; - /*Check for someone clearing wait stats*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 205 ) - AND @TraceFileIssue = 0 + IF @OutputType = 'COUNT' BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 205 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'Wait Stats Cleared Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. Why are you clearing wait stats? What are you hiding?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR)' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - + SELECT COUNT(*) AS Warnings + FROM #BlitzResults; END; + ELSE + IF @OutputType IN ( 'CSV', 'RSV' ) + BEGIN - /*Check for someone writing to pages. Yeah, right?*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 209 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 209 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'DBCC WRITEPAGE Used Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying to fix corruption, or cause corruption?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper = N'DBCC WRITEPAGE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - + SELECT Result = CAST([Priority] AS NVARCHAR(100)) + + @separator + CAST(CheckID AS NVARCHAR(100)) + + @separator + COALESCE([FindingsGroup], + '(N/A)') + @separator + + COALESCE([Finding], '(N/A)') + @separator + + COALESCE(DatabaseName, '(N/A)') + @separator + + COALESCE([URL], '(N/A)') + @separator + + COALESCE([Details], '(N/A)') + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + DatabaseName , + Details; + END; + ELSE IF @OutputXMLasNVARCHAR = 1 AND @OutputType <> 'NONE' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan, + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + DatabaseName , + Details; + END; + ELSE IF @OutputType = 'MARKDOWN' + BEGIN + WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * + FROM #BlitzResults + WHERE Priority > 0 AND Priority < 255 AND FindingsGroup IS NOT NULL AND Finding IS NOT NULL + AND FindingsGroup <> 'Security' /* Specifically excluding security checks for public exports */) + SELECT + Markdown = CONVERT(XML, STUFF((SELECT + CASE + WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf + ELSE N'' + END + + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> COALESCE(rNext.Finding,N'') THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf + WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END + ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END + END + @crlf + FROM Results r + LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 + LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 + ORDER BY r.rownum FOR XML PATH(N''), ROOT('Markdown'), TYPE).value('/Markdown[1]','VARCHAR(MAX)'), 1, 2, '') + + ''); + END; + ELSE IF @OutputType = 'XML' + BEGIN + /* --TOURSTOP05-- */ + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + DatabaseName , + Details + FOR XML PATH('Result'), ROOT('sp_Blitz_Output'); + END; + ELSE IF @OutputType <> 'NONE' + BEGIN + /* --TOURSTOP05-- */ + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + [QueryPlan] , + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + DatabaseName , + Details; END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 210 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 204) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + DROP TABLE #BlitzResults; - SELECT 210 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC SHRINK% Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying cause bad performance on purpose?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper LIKE N'DBCC SHRINK%' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; + IF @OutputProcedureCache = 1 + AND @CheckProcedureCache = 1 + SELECT TOP 20 + total_worker_time / execution_count AS AvgCPU , + total_worker_time AS TotalCPU , + CAST(ROUND(100.00 * total_worker_time + / ( SELECT SUM(total_worker_time) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentCPU , + total_elapsed_time / execution_count AS AvgDuration , + total_elapsed_time AS TotalDuration , + CAST(ROUND(100.00 * total_elapsed_time + / ( SELECT SUM(total_elapsed_time) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + CAST(ROUND(100.00 * total_logical_reads + / ( SELECT SUM(total_logical_reads) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentReads , + execution_count , + CAST(ROUND(100.00 * execution_count + / ( SELECT SUM(execution_count) + FROM sys.dm_exec_query_stats + ), 2) AS MONEY) AS PercentExecutions , + CASE WHEN DATEDIFF(mi, creation_time, + qs.last_execution_time) = 0 THEN 0 + ELSE CAST(( 1.00 * execution_count / DATEDIFF(mi, + creation_time, + qs.last_execution_time) ) AS MONEY) + END AS executions_per_minute , + qs.creation_time AS plan_creation_time , + qs.last_execution_time , + text , + text_filtered , + query_plan , + query_plan_filtered , + sql_handle , + query_hash , + plan_handle , + query_plan_hash + FROM #dm_exec_query_stats qs + ORDER BY CASE UPPER(@CheckProcedureCacheFilter) + WHEN 'CPU' THEN total_worker_time + WHEN 'READS' THEN total_logical_reads + WHEN 'EXECCOUNT' THEN execution_count + WHEN 'DURATION' THEN total_elapsed_time + ELSE total_worker_time + END DESC; - END; + END; /* ELSE -- IF @OutputType = 'SCHEMA' */ + /* + Cleanups - drop temporary tables that have been created by this SP. + */ + + IF(OBJECT_ID('tempdb..#InvalidLogins') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #InvalidLogins;'; + END; -/*End: checking default trace for odd DBCC activity*/ - - /*Begin check for autoshrink events*/ + IF OBJECT_ID('tempdb..#AlertInfo') IS NOT NULL + BEGIN + EXEC sp_executesql N'DROP TABLE #AlertInfo;'; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 206 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + /* + Reset the Nmumeric_RoundAbort session state back to enabled if it was disabled earlier. + See Github issue #2302 for more info. + */ + IF @NeedToTurnNumericRoundabortBackOn = 1 + SET NUMERIC_ROUNDABORT ON; - SELECT 206 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Ran Recently' AS Finding , - '' AS URL , - N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' - + CONVERT(NVARCHAR(10), COUNT(*)) - + N' auto shrink events between ' - + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) - + ' that lasted on average ' - + CONVERT(NVARCHAR(10), AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime))) - + ' seconds.' AS Details - FROM #fnTraceGettable AS t - WHERE t.EventClass IN (94, 95) - GROUP BY t.DatabaseName - HAVING AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime)) > 5; - - END; + SET NOCOUNT OFF; +GO +/* +--Sample execution call with the most common parameters: +EXEC [dbo].[sp_Blitz] + @CheckUserDatabaseObjects = 1 , + @CheckProcedureCache = 0 , + @OutputType = 'TABLE' , + @OutputProcedureCache = 0 , + @CheckProcedureCacheFilter = NULL, + @CheckServerInfo = 1 +*/ +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON - IF @CheckUserDatabaseObjects = 1 - BEGIN +IF NOT EXISTS (SELECT * FROM sys.objects WHERE [object_id] = OBJECT_ID(N'[dbo].[sp_BlitzAnalysis]') AND [type] in (N'P', N'PC')) +BEGIN +EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[sp_BlitzAnalysis] AS' +END +GO - IF @Debug IN (1, 2) RAISERROR('Starting @CheckUserDatabaseObjects section.', 0, 1) WITH NOWAIT +ALTER PROCEDURE [dbo].[sp_BlitzAnalysis] ( +@Help TINYINT = 0, +@StartDate DATETIMEOFFSET(7) = NULL, +@EndDate DATETIMEOFFSET(7) = NULL, +@OutputDatabaseName NVARCHAR(256) = 'DBAtools', +@OutputSchemaName NVARCHAR(256) = N'dbo', +@OutputTableNameBlitzFirst NVARCHAR(256) = N'BlitzFirst', +@OutputTableNameFileStats NVARCHAR(256) = N'BlitzFirst_FileStats', +@OutputTableNamePerfmonStats NVARCHAR(256) = N'BlitzFirst_PerfmonStats', +@OutputTableNameWaitStats NVARCHAR(256) = N'BlitzFirst_WaitStats', +@OutputTableNameBlitzCache NVARCHAR(256) = N'BlitzCache', +@OutputTableNameBlitzWho NVARCHAR(256) = N'BlitzWho', +@Servername NVARCHAR(128) = @@SERVERNAME, +@Databasename NVARCHAR(128) = NULL, +@BlitzCacheSortorder NVARCHAR(20) = N'cpu', +@MaxBlitzFirstPriority INT = 249, +@ReadLatencyThreshold INT = 100, +@WriteLatencyThreshold INT = 100, +@WaitStatsTop TINYINT = 10, +@Version VARCHAR(30) = NULL OUTPUT, +@VersionDate DATETIME = NULL OUTPUT, +@VersionCheckMode BIT = 0, +@BringThePain BIT = 0, +@Maxdop INT = 1, +@Debug BIT = 0 +) +AS +SET NOCOUNT ON; +SET STATISTICS XML OFF; - /* - But what if you need to run a query in every individual database? - Check out CheckID 99 below. Yes, it uses sp_MSforeachdb, and no, - we're not happy about that. sp_MSforeachdb is known to have a lot - of issues, like skipping databases sometimes. However, this is the - only built-in option that we have. If you're writing your own code - for database maintenance, consider Aaron Bertrand's alternative: - http://www.mssqltips.com/sqlservertip/2201/making-a-more-reliable-and-flexible-spmsforeachdb/ - We don't include that as part of sp_Blitz, of course, because - copying and distributing copyrighted code from others without their - written permission isn't a good idea. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 99 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; 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://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 - databases. We're not checking #SkipChecks here for each database to - see if we should run the check in this database. That means we may - still run a skipped check if it involves sp_MSforeachdb. We just - don't output those results in the last step. - */ +SELECT @Version = '8.26', @VersionDate = '20251002'; +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 163 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') - BEGIN - /* --TOURSTOP03-- */ +IF (@Help = 1) +BEGIN + PRINT 'EXEC sp_BlitzAnalysis +@StartDate = NULL, /* Specify a datetime or NULL will get an hour ago */ +@EndDate = NULL, /* Specify a datetime or NULL will get an hour of data since @StartDate */ +@OutputDatabaseName = N''DBA'', /* Specify the database name where where we can find your logged blitz data */ +@OutputSchemaName = N''dbo'', /* Specify the schema */ +@OutputTableNameBlitzFirst = N''BlitzFirst'', /* Table name where you are storing sp_BlitzFirst output, Set to NULL to ignore */ +@OutputTableNameFileStats = N''BlitzFirst_FileStats'', /* Table name where you are storing sp_BlitzFirst filestats output, Set to NULL to ignore */ +@OutputTableNamePerfmonStats = N''BlitzFirst_PerfmonStats'', /* Table name where you are storing sp_BlitzFirst Perfmon output, Set to NULL to ignore */ +@OutputTableNameWaitStats = N''BlitzFirst_WaitStats'', /* Table name where you are storing sp_BlitzFirst Wait stats output, Set to NULL to ignore */ +@OutputTableNameBlitzCache = N''BlitzCache'', /* Table name where you are storing sp_BlitzCache output, Set to NULL to ignore */ +@OutputTableNameBlitzWho = N''BlitzWho'', /* Table name where you are storing sp_BlitzWho output, Set to NULL to ignore */ +@Databasename = NULL, /* Filters results for BlitzCache, FileStats (will also include tempdb), BlitzWho. Leave as NULL for all databases */ +@MaxBlitzFirstPriority = 249, /* Max priority to include in the results */ +@BlitzCacheSortorder = ''cpu'', /* Accepted values ''all'' ''cpu'' ''reads'' ''writes'' ''duration'' ''executions'' ''memory grant'' ''spills'' */ +@WaitStatsTop = 3, /* Controls the top for wait stats only */ +@Maxdop = 1, /* Control the degree of parallelism that the queries within this proc can use if they want to*/ +@Debug = 0; /* Show sp_BlitzAnalysis SQL commands in the messages tab as they execute */ - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 163) WITH NOWAIT; +/* +Additional parameters: +@ReadLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_read_average_ms in your filestats table */ +@WriteLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_write_average_ms in your filestats table */ +@BringThePain BIT /* Default: 0 - If you are getting more than 4 hours of data with blitzcachesortorder set to ''all'' you will need to set BringThePain to 1 */ +*/'; + RETURN; +END - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 163, - ''?'', - 200, - ''Performance'', - ''Query Store Disabled'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; - END; +/* Declare all local variables required */ +DECLARE @FullOutputTableNameBlitzFirst NVARCHAR(1000); +DECLARE @FullOutputTableNameFileStats NVARCHAR(1000); +DECLARE @FullOutputTableNamePerfmonStats NVARCHAR(1000); +DECLARE @FullOutputTableNameWaitStats NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzCache NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzWho NVARCHAR(1000); +DECLARE @Sql NVARCHAR(MAX); +DECLARE @NewLine NVARCHAR(2) = CHAR(13); +DECLARE @IncludeMemoryGrants BIT; +DECLARE @IncludeSpills BIT; + +/* Validate the database name */ +IF (DB_ID(@OutputDatabaseName) IS NULL) +BEGIN + RAISERROR('Invalid database name provided for parameter @OutputDatabaseName: %s',11,0,@OutputDatabaseName); + RETURN; +END - - IF @ProductVersionMajor >= 13 AND @ProductVersionMinor < 2149 --CU1 has the fix in it - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 182 ) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 182) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 - 182, - ''Server'', - 20, - ''Reliability'', - ''Query Store Cleanup Disabled'', - ''https://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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; +/* Set fully qualified table names */ +SET @FullOutputTableNameBlitzFirst = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzFirst); +SET @FullOutputTableNameFileStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameFileStats+N'_Deltas'); +SET @FullOutputTableNamePerfmonStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNamePerfmonStats+N'_Actuals'); +SET @FullOutputTableNameWaitStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameWaitStats+N'_Deltas'); +SET @FullOutputTableNameBlitzCache = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzCache); +SET @FullOutputTableNameBlitzWho = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzWho+N'_Deltas'); - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 41 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 41) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 41, - ''?'', - 170, - ''File Configuration'', - ''Multiple Log Files on One Drive'', - ''https://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 ''?'' <> ''[tempdb]'' - GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 OPTION (RECOMPILE);'; - END; +IF OBJECT_ID('tempdb.dbo.#BlitzFirstCounts') IS NOT NULL +BEGIN + DROP TABLE #BlitzFirstCounts; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 42 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 42) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 42, - ''?'', - 170, - ''File Configuration'', - ''Uneven File Growth Settings in One Filegroup'', - ''https://BrentOzar.com/go/grow'', - (''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'' - GROUP BY data_space_id - HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; - END; +CREATE TABLE #BlitzFirstCounts ( + [Priority] TINYINT NOT NULL, + [FindingsGroup] VARCHAR(50) NOT NULL, + [Finding] VARCHAR(200) NOT NULL, + [TotalOccurrences] INT NULL, + [FirstOccurrence] DATETIMEOFFSET(7) NULL, + [LastOccurrence] DATETIMEOFFSET(7) NULL +); +/* Validate variables and set defaults as required */ +IF (@BlitzCacheSortorder IS NULL) +BEGIN + SET @BlitzCacheSortorder = N'cpu'; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 82 ) - BEGIN +SET @BlitzCacheSortorder = LOWER(@BlitzCacheSortorder); - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 82) WITH NOWAIT; +IF (@OutputTableNameBlitzCache IS NOT NULL AND @BlitzCacheSortorder NOT IN (N'all',N'cpu',N'reads',N'writes',N'duration',N'executions',N'memory grant',N'spills')) +BEGIN + RAISERROR('Invalid sort option specified for @BlitzCacheSortorder, supported values are ''all'', ''cpu'', ''reads'', ''writes'', ''duration'', ''executions'', ''memory grant'', ''spills''',11,0) WITH NOWAIT; + RETURN; +END - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 82 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to percent'', - ''https://BrentOzar.com/go/percentgrowth'' AS URL, - ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CAST((CONVERT(BIGINT, f.size * 8) / 1000000) AS NVARCHAR(10)) + '' 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);'; - END; +/* Set @Maxdop to 1 if NULL was passed in */ +IF (@Maxdop IS NULL) +BEGIN + SET @Maxdop = 1; +END +/* iF @Maxdop is set higher than the core count just set it to 0 */ +IF (@Maxdop > (SELECT CAST(cpu_count AS INT) FROM sys.dm_os_sys_info)) +BEGIN + SET @Maxdop = 0; +END +/* We need to check if your SQL version has memory grant and spills columns in sys.dm_exec_query_stats */ +SELECT @IncludeMemoryGrants = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb')) THEN 1 + ELSE 0 + END; - /* addition by Henrik Staun Poulsen, Stovi Software */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 158 ) - BEGIN +SELECT @IncludeSpills = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills')) THEN 1 + ELSE 0 + END; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 158) WITH NOWAIT; - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 158 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to 1MB'', - ''https://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((f.size * 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);'; - END; +IF (@StartDate IS NULL) +BEGIN + RAISERROR('Setting @StartDate to: 1 hour ago',0,0) WITH NOWAIT; + /* Set StartDate to be an hour ago */ + SET @StartDate = DATEADD(HOUR,-1,SYSDATETIMEOFFSET()); + IF (@EndDate IS NULL) + BEGIN + RAISERROR('Setting @EndDate to: Now',0,0) WITH NOWAIT; + /* Get data right up to now */ + SET @EndDate = SYSDATETIMEOFFSET(); + END +END +IF (@EndDate IS NULL) +BEGIN + /* Default to an hour of data or SYSDATETIMEOFFSET() if now is earlier than the hour added to @StartDate */ + IF(DATEADD(HOUR,1,@StartDate) < SYSDATETIMEOFFSET()) + BEGIN + RAISERROR('@EndDate was NULL - Setting to return 1 hour of information, if you want more then set @EndDate aswell',0,0) WITH NOWAIT; + SET @EndDate = DATEADD(HOUR,1,@StartDate); + END + ELSE + BEGIN + RAISERROR('@EndDate was NULL - Setting to SYSDATETIMEOFFSET()',0,0) WITH NOWAIT; + SET @EndDate = SYSDATETIMEOFFSET(); + END +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 33 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 33) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 33, - db_name(), - 200, - ''Licensing'', - ''Enterprise Edition Features In Use'', - ''https://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);'; - END; - END; +/* Default to dbo schema if NULL is passed in */ +IF (@OutputSchemaName IS NULL) +BEGIN + SET @OutputSchemaName = 'dbo'; +END +/* Prompt the user for @BringThePain = 1 if they are searching a timeframe greater than 4 hours and they are using BlitzCacheSortorder = 'all' */ +IF(@BlitzCacheSortorder = 'all' AND DATEDIFF(HOUR,@StartDate,@EndDate) > 4 AND @BringThePain = 0) +BEGIN + RAISERROR('Wow! hold up now, are you sure you wanna do this? Are sure you want to query over 4 hours of data with @BlitzCacheSortorder set to ''all''? IF you do then set @BringThePain = 1 but I gotta warn you this might hurt a bit!',11,1) WITH NOWAIT; + RETURN; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 19 ) - BEGIN - /* Method 1: Check sys.databases parameters */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 19) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) +/* Output report window information */ +SELECT + @Servername AS [ServerToReportOn], + CAST(1 AS NVARCHAR(20)) + N' - '+ CAST(@MaxBlitzFirstPriority AS NVARCHAR(20)) AS [PrioritesToInclude], + @StartDate AS [StartDatetime], + @EndDate AS [EndDatetime];; - SELECT 19 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Replication In Use' AS Finding , - 'https://BrentOzar.com/go/repl' AS URL , - ( 'Database [' + [name] - + '] is a replication publisher, subscriber, or distributor.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 19) - AND is_published = 1 - OR is_subscribed = 1 - OR is_merge_published = 1 - OR is_distributor = 1; - /* Method B: check subscribers for MSreplication_objects tables */ - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 19, - db_name(), - 200, - ''Informational'', - ''Replication In Use'', - ''https://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)'; +/* BlitzFirst data */ +SET @Sql = N' +INSERT INTO #BlitzFirstCounts ([Priority],[FindingsGroup],[Finding],[TotalOccurrences],[FirstOccurrence],[LastOccurrence]) +SELECT +[Priority], +[FindingsGroup], +[Finding], +COUNT(*) AS [TotalOccurrences], +MIN(CheckDate) AS [FirstOccurrence], +MAX(CheckDate) AS [LastOccurrence] +FROM '+@FullOutputTableNameBlitzFirst+N' +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND CheckDate BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +GROUP BY [Priority],[FindingsGroup],[Finding]; + +IF EXISTS(SELECT 1 FROM #BlitzFirstCounts) +BEGIN + SELECT + [Priority], + [FindingsGroup], + [Finding], + [TotalOccurrences], + [FirstOccurrence], + [LastOccurrence] + FROM #BlitzFirstCounts + ORDER BY [Priority] ASC,[TotalOccurrences] DESC; +END +ELSE +BEGIN + SELECT N''No findings with a priority between 1 and ''+CAST(@MaxBlitzFirstPriority AS NVARCHAR(10))+N'' found for this period''; +END +SELECT + [ServerName] +,[CheckDate] +,[CheckID] +,[Priority] +,[Finding] +,[URL] +,[Details] +,[HowToStopIt] +,[QueryPlan] +,[QueryText] +FROM '+@FullOutputTableNameBlitzFirst+N' Findings +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND [CheckDate] BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +ORDER BY CheckDate ASC,[Priority] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + + +RAISERROR('Getting BlitzFirst info from %s',0,0,@FullOutputTableNameBlitzFirst) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END - END; +IF (OBJECT_ID(@FullOutputTableNameBlitzFirst) IS NULL) +BEGIN + IF (@OutputTableNameBlitzFirst IS NULL) + BEGIN + RAISERROR('BlitzFirst data skipped',10,0); + SELECT N'Skipped logged BlitzFirst data as NULL was passed to parameter @OutputTableNameBlitzFirst'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzFirst data: %s does not exist',10,0,@FullOutputTableNameBlitzFirst); + SELECT N'No BlitzFirst data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @MaxBlitzFirstPriority INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @MaxBlitzFirstPriority = @MaxBlitzFirstPriority; +END +/* Blitz WaitStats data */ +SET @Sql = N'SELECT +[ServerName], +[CheckDate], +[wait_type], +[WaitsRank], +[WaitCategory], +[Ignorable], +[ElapsedSeconds], +[wait_time_ms_delta], +[wait_time_minutes_delta], +[wait_time_minutes_per_minute], +[signal_wait_time_ms_delta], +[waiting_tasks_count_delta], +ISNULL((CAST([wait_time_ms_delta] AS DECIMAL(38,2))/NULLIF(CAST([waiting_tasks_count_delta] AS DECIMAL(38,2)),0)),0) AS [wait_time_ms_per_wait] +FROM +( + SELECT + [ServerName], + [CheckDate], + [wait_type], + [WaitCategory], + [Ignorable], + [ElapsedSeconds], + [wait_time_ms_delta], + [wait_time_minutes_delta], + [wait_time_minutes_per_minute], + [signal_wait_time_ms_delta], + [waiting_tasks_count_delta], + ROW_NUMBER() OVER(PARTITION BY [CheckDate] ORDER BY [CheckDate] ASC,[wait_time_ms_delta] DESC) AS [WaitsRank] + FROM '+@FullOutputTableNameWaitStats+N' AS [Waits] + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate +) TopWaits +WHERE [WaitsRank] <= @WaitStatsTop +ORDER BY +[CheckDate] ASC, +[wait_time_ms_delta] DESC +OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting wait stats info from %s',0,0,@FullOutputTableNameWaitStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 32 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 32) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 32, - ''?'', - 150, - ''Performance'', - ''Triggers on Tables'', - ''https://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'' - HAVING SUM(1) > 0 OPTION (RECOMPILE)'; - END; +IF (OBJECT_ID(@FullOutputTableNameWaitStats) IS NULL) +BEGIN + IF (@OutputTableNameWaitStats IS NULL) + BEGIN + RAISERROR('Wait stats data skipped',10,0); + SELECT N'Skipped logged wait stats data as NULL was passed to parameter @OutputTableNameWaitStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for wait stats data: %s does not exist',10,0,@FullOutputTableNameWaitStats); + SELECT N'No wait stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @WaitStatsTop TINYINT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @WaitStatsTop=@WaitStatsTop; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 38 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 38) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 38, - ''?'', - 110, - ''Performance'', - ''Active Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that are being actively queried.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NOT NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; +/* BlitzFileStats info */ +SET @Sql = N' +SELECT +[ServerName], +[CheckDate], +CASE + WHEN MAX([io_stall_read_ms_average]) > @ReadLatencyThreshold THEN ''Yes'' + WHEN MAX([io_stall_write_ms_average]) > @WriteLatencyThreshold THEN ''Yes'' + ELSE ''No'' +END AS [io_stall_ms_breached], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) AS [PhysicalPath], +SUM([SizeOnDiskMB]) AS [SizeOnDiskMB], +SUM([SizeOnDiskMBgrowth]) AS [SizeOnDiskMBgrowth], +MAX([io_stall_read_ms]) AS [max_io_stall_read_ms], +MAX([io_stall_read_ms_average]) AS [max_io_stall_read_ms_average], +@ReadLatencyThreshold AS [is_stall_read_ms_threshold], +SUM([num_of_reads]) AS [num_of_reads], +SUM([megabytes_read]) AS [megabytes_read], +MAX([io_stall_write_ms]) AS [max_io_stall_write_ms], +MAX([io_stall_write_ms_average]) AS [max_io_stall_write_ms_average], +@WriteLatencyThreshold AS [io_stall_write_ms_average], +SUM([num_of_writes]) AS [num_of_writes], +SUM([megabytes_written]) AS [megabytes_written] +FROM '+@FullOutputTableNameFileStats+N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate +' ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] IN (N''tempdb'',@Databasename) +' + ELSE N'' +END ++N'GROUP BY +[ServerName], +[CheckDate], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) +ORDER BY +[CheckDate] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 164 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'fn_validate_plan_guide') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 164) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 164, - ''?'', - 20, - ''Reliability'', - ''Plan Guides Failing'', - ''https://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)'; - END; +RAISERROR('Getting FileStats info from %s',0,0,@FullOutputTableNameFileStats) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 39 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 39) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 39, - ''?'', - 150, - ''Performance'', - ''Inactive Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that have not been queried since the last restart. These may be backup tables carelessly left behind.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 46 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 46) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 46, - ''?'', - 150, - ''Performance'', - ''Leftover Fake Indexes From Wizards'', - ''https://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);'; - END; +IF (OBJECT_ID(@FullOutputTableNameFileStats) IS NULL) +BEGIN + IF (@OutputTableNameFileStats IS NULL) + BEGIN + RAISERROR('File stats data skipped',10,0); + SELECT N'Skipped logged File stats data as NULL was passed to parameter @OutputTableNameFileStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for FileStats data: %s does not exist',10,0,@FullOutputTableNameFileStats); + SELECT N'No File stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @ReadLatencyThreshold INT, + @WriteLatencyThreshold INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename, + @ReadLatencyThreshold = @ReadLatencyThreshold, + @WriteLatencyThreshold = @WriteLatencyThreshold; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 47 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 47) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 47, - ''?'', - 100, - ''Performance'', - ''Indexes Disabled'', - ''https://BrentOzar.com/go/ixoff'', - (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1 OPTION (RECOMPILE);'; - END; +/* Blitz Perfmon stats*/ +SET @Sql = N' +SELECT + [ServerName] + ,[CheckDate] + ,[counter_name] + ,[object_name] + ,[instance_name] + ,[cntr_value] +FROM '+@FullOutputTableNamePerfmonStats+N' +WHERE [ServerName] = @Servername +AND CheckDate BETWEEN @StartDate AND @EndDate +ORDER BY + [CheckDate] ASC, + [counter_name] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting Perfmon info from %s',0,0,@FullOutputTableNamePerfmonStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END +IF (OBJECT_ID(@FullOutputTableNamePerfmonStats) IS NULL) +BEGIN + IF (@OutputTableNamePerfmonStats IS NULL) + BEGIN + RAISERROR('Perfmon stats data skipped',10,0); + SELECT N'Skipped logged Perfmon stats data as NULL was passed to parameter @OutputTableNamePerfmonStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for Perfmon stats data: %s does not exist',10,0,@FullOutputTableNamePerfmonStats); + SELECT N'No Perfmon data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 48 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 48) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 48, - ''?'', - 150, - ''Performance'', - ''Foreign Keys Not Trusted'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE);'; - END; +/* Blitz cache data */ +RAISERROR('Sortorder for BlitzCache data: %s',0,0,@BlitzCacheSortorder) WITH NOWAIT; + +/* Set intial CTE */ +SET @Sql = N'WITH CheckDates AS ( +SELECT DISTINCT CheckDate +FROM ' ++@FullOutputTableNameBlitzCache ++N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate' ++@NewLine ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] = @Databasename'+@NewLine + ELSE N'' +END ++N')' +; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 56 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 56) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 56, - ''?'', - 150, - ''Performance'', - ''Check Constraint Not Trusted'', - ''https://BrentOzar.com/go/trust'', - (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') - from [?].sys.check_constraints 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 OPTION (RECOMPILE);'; - END; +SET @Sql += @NewLine; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 95 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 95) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 95 AS CheckID, - ''?'' as DatabaseName, - 110 AS Priority, - ''Performance'' AS FindingsGroup, - ''Plan Guides Enabled'' AS Finding, - ''https://BrentOzar.com/go/guides'' AS URL, - (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details - FROM [?].sys.plan_guides WHERE is_disabled = 0 OPTION (RECOMPILE);'; - END; - END; +/* Append additional CTEs based on sortorder */ +SET @Sql += ( +SELECT CAST(N',' AS NVARCHAR(MAX)) ++[SortOptions].[Aliasname]+N' AS ( +SELECT + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,[Sortorder] + ,[TimeFrameRank] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [OverallRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] +FROM CheckDates +CROSS APPLY ( + SELECT TOP (5) + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,'+QUOTENAME(UPPER([SortOptions].[Sortorder]),N'''')+N' AS [Sortorder] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [TimeFrameRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] + FROM '+@FullOutputTableNameBlitzCache+N' AS '+[SortOptions].[Aliasname]+N' + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate + AND ['+[SortOptions].[Aliasname]+N'].[CheckDate] = [CheckDates].[CheckDate]' + +@NewLine + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND ['+[SortOptions].[Aliasname]+N'].[DatabaseName] = @Databasename'+@NewLine + ELSE N'' + END + +CASE + WHEN [Sortorder] = N'cpu' THEN N'AND [TotalCPU] > 0' + WHEN [Sortorder] = N'reads' THEN N'AND [TotalReads] > 0' + WHEN [Sortorder] = N'writes' THEN N'AND [TotalWrites] > 0' + WHEN [Sortorder] = N'duration' THEN N'AND [TotalDuration] > 0' + WHEN [Sortorder] = N'executions' THEN N'AND [ExecutionCount] > 0' + WHEN [Sortorder] = N'memory grant' THEN N'AND [MaxGrantKB] > 0' + WHEN [Sortorder] = N'spills' THEN N'AND [MaxSpills] > 0' + ELSE N'' + END + +N' + ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) '+[SortOptions].[Aliasname]+N' +)' +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'); + +SET @Sql += @NewLine; + +/* Build the select statements to return the data after CTE declarations */ +SET @Sql += ( +SELECT STUFF(( +SELECT @NewLine ++N'UNION ALL' ++@NewLine ++N'SELECT * +FROM '+[SortOptions].[Aliasname] +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'),1,11,N'') +); - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 60 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 60) WITH NOWAIT; - - EXEC sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 60 AS CheckID, - ''?'' as DatabaseName, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Fill Factor Changed'', - ''https://BrentOzar.com/go/fillfactor'' AS URL, - ''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' objects with fill factor = '' + CAST(fill_factor AS NVARCHAR(5)) + ''%. This can cause memory and storage performance problems, but may also prevent page splits.'' - FROM [?].sys.indexes - WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 - GROUP BY fill_factor OPTION (RECOMPILE);'; - END; +/* Append Order By */ +SET @Sql += @NewLine ++N'ORDER BY + [Sortorder] ASC, + [CheckDate] ASC, + [TimeFrameRank] ASC'; +/* Append OPTION(RECOMPILE, MAXDOP) to complete the statement */ +SET @Sql += @NewLine ++N'OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; +RAISERROR('Getting BlitzCache info from %s',0,0,@FullOutputTableNameBlitzCache) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 78 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 78) WITH NOWAIT; - - EXECUTE master.sys.sp_MSforeachdb 'USE [?]; - INSERT INTO #Recompile - SELECT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA - FROM sys.sql_modules AS SM - LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() - LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' - LEFT OUTER JOIN INFORMATION_SCHEMA.ROUTINES AS ISR on ISR.Routine_Name = SO.name AND ISR.SPECIFIC_CATALOG = DB_Name() - WHERE SM.is_recompiled=1 OPTION (RECOMPILE); /* oh the rich irony of recompile here */ - '; - INSERT INTO #BlitzResults - (Priority, - FindingsGroup, - Finding, - DatabaseName, - URL, - Details, - CheckID) - SELECT [Priority] = '100', - FindingsGroup = 'Performance', - Finding = 'Stored Procedure WITH RECOMPILE', - DatabaseName = DBName, - URL = 'https://BrentOzar.com/go/recompile', - Details = '[' + DBName + '].[' + SPSchema + '].[' + ProcName + '] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.', - CheckID = '78' - FROM #Recompile AS TR WHERE ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%'; - DROP TABLE #Recompile; - END; +IF (@Debug = 1) +BEGIN + PRINT SUBSTRING(@Sql, 0, 4000); + PRINT SUBSTRING(@Sql, 4000, 8000); + PRINT SUBSTRING(@Sql, 8000, 12000); + PRINT SUBSTRING(@Sql, 12000, 16000); + PRINT SUBSTRING(@Sql, 16000, 20000); + PRINT SUBSTRING(@Sql, 20000, 24000); + PRINT SUBSTRING(@Sql, 24000, 28000); +END +IF (OBJECT_ID(@FullOutputTableNameBlitzCache) IS NULL) +BEGIN + IF (@OutputTableNameBlitzCache IS NULL) + BEGIN + RAISERROR('BlitzCache data skipped',10,0); + SELECT N'Skipped logged BlitzCache data as NULL was passed to parameter @OutputTableNameBlitzCache'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzCache data: %s does not exist',10,0,@FullOutputTableNameBlitzCache); + SELECT N'No BlitzCache data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @BlitzCacheSortorder NVARCHAR(20), + @StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7)', + @Servername = @Servername, + @Databasename = @Databasename, + @BlitzCacheSortorder = @BlitzCacheSortorder, + @StartDate = @StartDate, + @EndDate = @EndDate; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 86 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 86) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://BrentOzar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM [?].dbo.sysmembers m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; - END; +/* BlitzWho data */ +SET @Sql = N' +SELECT [ServerName] + ,[CheckDate] + ,[elapsed_time] + ,[session_id] + ,[database_name] + ,[query_text_snippet] + ,[query_plan] + ,[live_query_plan] + ,[query_cost] + ,[status] + ,[wait_info] + ,[top_session_waits] + ,[blocking_session_id] + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[degree_of_parallelism] + ,[request_logical_reads] + ,[Logical_Reads_MB] + ,[request_writes] + ,[Logical_Writes_MB] + ,[request_physical_reads] + ,[Physical_reads_MB] + ,[session_cpu] + ,[session_logical_reads] + ,[session_logical_reads_MB] + ,[session_physical_reads] + ,[session_physical_reads_MB] + ,[session_writes] + ,[session_writes_MB] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads] + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info] + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset] + FROM '+@FullOutputTableNameBlitzWho+N' + WHERE [ServerName] = @Servername + AND ([CheckDate] BETWEEN @StartDate AND @EndDate OR [start_time] BETWEEN CAST(@StartDate AS DATETIME) AND CAST(@EndDate AS DATETIME)) + ' + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND [database_name] = @Databasename + ' + ELSE N'' + END ++N'ORDER BY [CheckDate] ASC + OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + +RAISERROR('Getting BlitzWho info from %s',0,0,@FullOutputTableNameBlitzWho) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END - /*Check for non-aligned indexes in partioned databases*/ +IF (OBJECT_ID(@FullOutputTableNameBlitzWho) IS NULL) +BEGIN + IF (@OutputTableNameBlitzWho IS NULL) + BEGIN + RAISERROR('BlitzWho data skipped',10,0); + SELECT N'Skipped logged BlitzWho data as NULL was passed to parameter @OutputTableNameBlitzWho'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzWho data: %s does not exist',10,0,@FullOutputTableNameBlitzWho); + SELECT N'No BlitzWho data available as the table cannot be found'; + END +END +ELSE +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename; +END - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 72 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 72) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - insert into #partdb(dbname, objectname, type_desc) - SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc - FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id - JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id - LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() - WHERE o.type = ''u'' - -- Clustered and Non-Clustered indexes - AND i.type IN (1, 2) - AND o.object_id in - ( - SELECT a.object_id from - (SELECT ob.object_id, ds.type_desc from sys.objects ob JOIN sys.indexes ind on ind.object_id = ob.object_id join sys.data_spaces ds on ds.data_space_id = ind.data_space_id - GROUP BY ob.object_id, ds.type_desc ) a group by a.object_id having COUNT (*) > 1 - ) OPTION (RECOMPILE);'; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 72 AS CheckID , - dbname AS DatabaseName , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'The partitioned database ' + dbname - + ' may have non-aligned indexes' AS Finding , - 'https://BrentOzar.com/go/aligned' AS URL , - 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details - FROM #partdb - WHERE dbname IS NOT NULL - AND dbname NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 72); - DROP TABLE #partdb; - END; +GO +IF OBJECT_ID('dbo.sp_BlitzBackups') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzBackups AS RETURN 0;'); +GO +ALTER PROCEDURE [dbo].[sp_BlitzBackups] + @Help TINYINT = 0 , + @HoursBack INT = 168, + @MSDBName NVARCHAR(256) = 'msdb', + @AGName NVARCHAR(256) = NULL, + @RestoreSpeedFullMBps INT = NULL, + @RestoreSpeedDiffMBps INT = NULL, + @RestoreSpeedLogMBps INT = NULL, + @Debug TINYINT = 0, + @PushBackupHistoryToListener BIT = 0, + @WriteBackupsToListenerName NVARCHAR(256) = NULL, + @WriteBackupsToDatabaseName NVARCHAR(256) = NULL, + @WriteBackupsLastHours INT = 168, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 +WITH RECOMPILE +AS + BEGIN + SET NOCOUNT ON; + SET STATISTICS XML OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 113 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 113) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 113, - ''?'', - 50, - ''Reliability'', - ''Full Text Indexes Not Updating'', - ''https://BrentOzar.com/go/fulltext'', - (''At least one full text index in this database has not been crawled in the last week.'') - from [?].sys.fulltext_indexes i WHERE change_tracking_state_desc <> ''AUTO'' AND i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE()) OPTION (RECOMPILE);'; - END; + IF @Help = 1 PRINT ' + /* + sp_BlitzBackups from http://FirstResponderKit.org + + This script checks your backups to see how much data you might lose when + this server fails, and how long it might take to recover. - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 115 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 115) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 115, - ''?'', - 110, - ''Performance'', - ''Parallelism Rocket Surgery'', - ''https://BrentOzar.com/go/makeparallel'', - (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') - from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'' OPTION (RECOMPILE);'; - END; + 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. + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 122 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 122) WITH NOWAIT; - - /* SQL Server 2012 and newer uses temporary stats for AlwaysOn Availability Groups, and those show up as user-created */ - IF EXISTS (SELECT * - FROM sys.all_columns c - INNER JOIN sys.all_objects o ON c.object_id = o.object_id - WHERE c.name = 'is_temporary' AND o.name = 'stats') - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) - ELSE - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - END; /* IF NOT EXISTS ( SELECT 1 */ + Parameter explanations: + @HoursBack INT = 168 How many hours of history to examine, back from now. + You can check just the last 24 hours of backups, for example. + @MSDBName NVARCHAR(255) You can restore MSDB from different servers and check them + centrally. Also useful if you create a DBA utility database + and merge data from several servers in an AG into one DB. + @RestoreSpeedFullMBps INT By default, we use the backup speed from MSDB to guesstimate + how fast your restores will go. If you have done performance + tuning and testing of your backups (or if they horribly go even + slower in your DR environment, and you want to account for + that), then you can pass in different numbers here. + @RestoreSpeedDiffMBps INT See above. + @RestoreSpeedLogMBps INT See above. - /*Check for high VLF count: this will omit any database snapshots*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 69 ) - BEGIN - IF @ProductVersionMajor >= 11 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo2012 - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo2012 - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo2012;'; - DROP TABLE #LogInfo2012; - END; - ELSE - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (pre-2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo;'; - DROP TABLE #LogInfo; - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 80 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 80) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://BrentOzar.com/go/maxsize'', (''The ['' + DB_NAME() + ''] database file '' + name + '' has a max file size set to '' + CAST(CAST(max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') FROM sys.database_files WHERE max_size <> 268435456 AND max_size <> -1 AND type <> 2 AND name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; - END; + For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + MIT License - /* Check if columnstore indexes are in use - for Github issue #615 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ - BEGIN - TRUNCATE TABLE #TemporaryDatabaseResults; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; IF EXISTS(SELECT * FROM sys.indexes WHERE type IN (5,6)) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; - IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @ColumnStoreIndexesInUse = 1; - END; + Copyright (c) Brent Ozar Unlimited + 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: - /* Non-Default Database Scoped Config - Github issue #598 */ - IF EXISTS ( SELECT * FROM sys.all_objects WHERE [name] = 'database_scoped_configurations' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] through [%d].', 0, 1, 194, 197) 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; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) - SELECT def1.CheckID, DB_NAME(), 210, ''Non-Default Database Scoped Config'', dsc.[name], ''https://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 - LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (dsc.value = def.default_value 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);'; - END; + 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. - - END; /* IF @CheckUserDatabaseObjects = 1 */ - IF @CheckProcedureCache = 1 - - BEGIN - IF @Debug IN (1, 2) RAISERROR('Begin checking procedure cache', 0, 1) WITH NOWAIT; - - BEGIN + */'; +ELSE +BEGIN +DECLARE @StringToExecute NVARCHAR(MAX) = N'', + @InnerStringToExecute NVARCHAR(MAX) = N'', + @ProductVersion NVARCHAR(128), + @ProductVersionMajor DECIMAL(10, 2), + @ProductVersionMinor DECIMAL(10, 2), + @StartTime DATETIME2, @ResultText NVARCHAR(MAX), + @crlf NVARCHAR(2), + @MoreInfoHeader NVARCHAR(100), + @MoreInfoFooter NVARCHAR(100); - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 35 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 35) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 35 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Single-Use Plans in Procedure Cache' AS Finding , - 'https://BrentOzar.com/go/single' AS URL , - ( CAST(COUNT(*) AS VARCHAR(10)) - + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details - FROM sys.dm_exec_cached_plans AS cp - WHERE cp.usecounts = 1 - AND cp.objtype = 'Adhoc' - AND EXISTS ( SELECT - 1 - FROM sys.configurations - WHERE - name = 'optimize for ad hoc workloads' - AND value_in_use = 0 ) - HAVING COUNT(*) > 1; - END; +IF @HoursBack > 0 + SET @HoursBack = @HoursBack * -1; +IF @WriteBackupsLastHours > 0 + SET @WriteBackupsLastHours = @WriteBackupsLastHours * -1; - /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ - IF @@VERSION LIKE '%Microsoft SQL Server 2005%' - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +SELECT @crlf = NCHAR(13) + NCHAR(10), + @StartTime = DATEADD(hh, @HoursBack, GETDATE()), + @MoreInfoHeader = N''; - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); +SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), + @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +CREATE TABLE #Backups +( + id INT IDENTITY(1, 1), + database_name NVARCHAR(128), + database_guid UNIQUEIDENTIFIER, + RPOWorstCaseMinutes DECIMAL(18, 1), + RTOWorstCaseMinutes DECIMAL(18, 1), + RPOWorstCaseBackupSetID INT, + RPOWorstCaseBackupSetFinishTime DATETIME, + RPOWorstCaseBackupSetIDPrior INT, + RPOWorstCaseBackupSetPriorFinishTime DATETIME, + RPOWorstCaseMoreInfoQuery XML, + RTOWorstCaseBackupFileSizeMB DECIMAL(18, 2), + RTOWorstCaseMoreInfoQuery XML, + FullMBpsAvg DECIMAL(18, 2), + FullMBpsMin DECIMAL(18, 2), + FullMBpsMax DECIMAL(18, 2), + FullSizeMBAvg DECIMAL(18, 2), + FullSizeMBMin DECIMAL(18, 2), + FullSizeMBMax DECIMAL(18, 2), + FullCompressedSizeMBAvg DECIMAL(18, 2), + FullCompressedSizeMBMin DECIMAL(18, 2), + FullCompressedSizeMBMax DECIMAL(18, 2), + DiffMBpsAvg DECIMAL(18, 2), + DiffMBpsMin DECIMAL(18, 2), + DiffMBpsMax DECIMAL(18, 2), + DiffSizeMBAvg DECIMAL(18, 2), + DiffSizeMBMin DECIMAL(18, 2), + DiffSizeMBMax DECIMAL(18, 2), + DiffCompressedSizeMBAvg DECIMAL(18, 2), + DiffCompressedSizeMBMin DECIMAL(18, 2), + DiffCompressedSizeMBMax DECIMAL(18, 2), + LogMBpsAvg DECIMAL(18, 2), + LogMBpsMin DECIMAL(18, 2), + LogMBpsMax DECIMAL(18, 2), + LogSizeMBAvg DECIMAL(18, 2), + LogSizeMBMin DECIMAL(18, 2), + LogSizeMBMax DECIMAL(18, 2), + LogCompressedSizeMBAvg DECIMAL(18, 2), + LogCompressedSizeMBMin DECIMAL(18, 2), + LogCompressedSizeMBMax DECIMAL(18, 2) +); - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +CREATE TABLE #RTORecoveryPoints +( + id INT IDENTITY(1, 1), + database_name NVARCHAR(128), + database_guid UNIQUEIDENTIFIER, + rto_worst_case_size_mb AS + ( COALESCE(log_file_size_mb, 0) + COALESCE(diff_file_size_mb, 0) + COALESCE(full_file_size_mb, 0)), + rto_worst_case_time_seconds AS + ( COALESCE(log_time_seconds, 0) + COALESCE(diff_time_seconds, 0) + COALESCE(full_time_seconds, 0)), + full_backup_set_id INT, + full_last_lsn NUMERIC(25, 0), + full_backup_set_uuid UNIQUEIDENTIFIER, + full_time_seconds BIGINT, + full_file_size_mb DECIMAL(18, 2), + diff_backup_set_id INT, + diff_last_lsn NUMERIC(25, 0), + diff_time_seconds BIGINT, + diff_file_size_mb DECIMAL(18, 2), + log_backup_set_id INT, + log_last_lsn NUMERIC(25, 0), + log_time_seconds BIGINT, + log_file_size_mb DECIMAL(18, 2), + log_backups INT +); - END; - IF @ProductVersionMajor >= 10 - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +CREATE TABLE #Recoverability + ( + Id INT IDENTITY , + DatabaseName NVARCHAR(128), + DatabaseGUID UNIQUEIDENTIFIER, + LastBackupRecoveryModel NVARCHAR(60), + FirstFullBackupSizeMB DECIMAL (18,2), + FirstFullBackupDate DATETIME, + LastFullBackupSizeMB DECIMAL (18,2), + LastFullBackupDate DATETIME, + AvgFullBackupThroughputMB DECIMAL (18,2), + AvgFullBackupDurationSeconds INT, + AvgDiffBackupThroughputMB DECIMAL (18,2), + AvgDiffBackupDurationSeconds INT, + AvgLogBackupThroughputMB DECIMAL (18,2), + AvgLogBackupDurationSeconds INT, + AvgFullSizeMB DECIMAL (18,2), + AvgDiffSizeMB DECIMAL (18,2), + AvgLogSizeMB DECIMAL (18,2), + IsBigDiff AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgDiffSizeMB * 100.) / AvgFullSizeMB >= 40.)) THEN 1 ELSE 0 END, + IsBigLog AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgLogSizeMB * 100.) / AvgFullSizeMB >= 20.)) THEN 1 ELSE 0 END + ); - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +CREATE TABLE #Trending +( + DatabaseName NVARCHAR(128), + DatabaseGUID UNIQUEIDENTIFIER, + [0] DECIMAL(18, 2), + [-1] DECIMAL(18, 2), + [-2] DECIMAL(18, 2), + [-3] DECIMAL(18, 2), + [-4] DECIMAL(18, 2), + [-5] DECIMAL(18, 2), + [-6] DECIMAL(18, 2), + [-7] DECIMAL(18, 2), + [-8] DECIMAL(18, 2), + [-9] DECIMAL(18, 2), + [-10] DECIMAL(18, 2), + [-11] DECIMAL(18, 2), + [-12] DECIMAL(18, 2) +); - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; +CREATE TABLE #Warnings +( + Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + CheckId INT, + Priority INT, + DatabaseName VARCHAR(128), + Finding VARCHAR(256), + Warning VARCHAR(8000) +); - /* Populate the query_plan_filtered field. Only works in 2005SP2+, but we're just doing it in 2008 to be safe. */ - UPDATE #dm_exec_query_stats - SET query_plan_filtered = qp.query_plan - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, - qs.statement_start_offset, - qs.statement_end_offset) - AS qp; +IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = @MSDBName) + BEGIN + RAISERROR('@MSDBName was specified, but the database does not exist.', 16, 1) WITH NOWAIT; + RETURN; + END - END; +IF @PushBackupHistoryToListener = 1 +GOTO PushBackupHistoryToListener - /* Populate the additional query_plan, text, and text_filtered fields */ - UPDATE #dm_exec_query_stats - SET query_plan = qp.query_plan , - [text] = st.[text] , - text_filtered = SUBSTRING(st.text, - ( qs.statement_start_offset - / 2 ) + 1, - ( ( CASE qs.statement_end_offset - WHEN -1 - THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - - qs.statement_start_offset ) - / 2 ) + 1) - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) - AS qp; - /* Dump instances of our own script. We're not trying to tune ourselves. */ - DELETE #dm_exec_query_stats - WHERE text LIKE '%sp_Blitz%' - OR text LIKE '%#BlitzResults%'; + RAISERROR('Inserting to #Backups', 0, 1) WITH NOWAIT; - /* Look for implicit conversions */ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 63 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 63) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 63 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%CONVERT_IMPLICIT%' - AND COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%PhysicalOp="Index Scan"%'; - END; + SET @StringToExecute += N'WITH Backups AS (SELECT bs.database_name, bs.database_guid, bs.type AS backup_type ' + @crlf + + ' , MBpsAvg = CAST(AVG(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf + + ' , MBpsMin = CAST(MIN(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf + + ' , MBpsMax = CAST(MAX(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf + + ' , SizeMBAvg = AVG(backup_size / 1048576.0) ' + @crlf + + ' , SizeMBMin = MIN(backup_size / 1048576.0) ' + @crlf + + ' , SizeMBMax = MAX(backup_size / 1048576.0) ' + @crlf + + ' , CompressedSizeMBAvg = AVG(compressed_backup_size / 1048576.0) ' + @crlf + + ' , CompressedSizeMBMin = MIN(compressed_backup_size / 1048576.0) ' + @crlf + + ' , CompressedSizeMBMax = MAX(compressed_backup_size / 1048576.0) ' + @crlf; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 64 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 64) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 64 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion Affecting Cardinality' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= @StartTime AND bs.is_damaged = 0 ' + @crlf + + N' GROUP BY bs.database_name, bs.database_guid, bs.type)' + @crlf; + SET @StringToExecute += + N'INSERT INTO #Backups(database_name, database_guid, ' + @crlf + + N' FullMBpsAvg, FullMBpsMin, FullMBpsMax, FullSizeMBAvg, FullSizeMBMin, FullSizeMBMax, FullCompressedSizeMBAvg, FullCompressedSizeMBMin, FullCompressedSizeMBMax, ' + @crlf + + N' DiffMBpsAvg, DiffMBpsMin, DiffMBpsMax, DiffSizeMBAvg, DiffSizeMBMin, DiffSizeMBMax, DiffCompressedSizeMBAvg, DiffCompressedSizeMBMin, DiffCompressedSizeMBMax, ' + @crlf + + N' LogMBpsAvg, LogMBpsMin, LogMBpsMax, LogSizeMBAvg, LogSizeMBMin, LogSizeMBMax, LogCompressedSizeMBAvg, LogCompressedSizeMBMin, LogCompressedSizeMBMax ) ' + @crlf + + N'SELECT bF.database_name, bF.database_guid ' + @crlf + + N' , bF.MBpsAvg AS FullMBpsAvg ' + @crlf + + N' , bF.MBpsMin AS FullMBpsMin ' + @crlf + + N' , bF.MBpsMax AS FullMBpsMax ' + @crlf + + N' , bF.SizeMBAvg AS FullSizeMBAvg ' + @crlf + + N' , bF.SizeMBMin AS FullSizeMBMin ' + @crlf + + N' , bF.SizeMBMax AS FullSizeMBMax ' + @crlf + + N' , bF.CompressedSizeMBAvg AS FullCompressedSizeMBAvg ' + @crlf + + N' , bF.CompressedSizeMBMin AS FullCompressedSizeMBMin ' + @crlf + + N' , bF.CompressedSizeMBMax AS FullCompressedSizeMBMax ' + @crlf + + N' , bD.MBpsAvg AS DiffMBpsAvg ' + @crlf + + N' , bD.MBpsMin AS DiffMBpsMin ' + @crlf + + N' , bD.MBpsMax AS DiffMBpsMax ' + @crlf + + N' , bD.SizeMBAvg AS DiffSizeMBAvg ' + @crlf + + N' , bD.SizeMBMin AS DiffSizeMBMin ' + @crlf + + N' , bD.SizeMBMax AS DiffSizeMBMax ' + @crlf + + N' , bD.CompressedSizeMBAvg AS DiffCompressedSizeMBAvg ' + @crlf + + N' , bD.CompressedSizeMBMin AS DiffCompressedSizeMBMin ' + @crlf + + N' , bD.CompressedSizeMBMax AS DiffCompressedSizeMBMax ' + @crlf + + N' , bL.MBpsAvg AS LogMBpsAvg ' + @crlf + + N' , bL.MBpsMin AS LogMBpsMin ' + @crlf + + N' , bL.MBpsMax AS LogMBpsMax ' + @crlf + + N' , bL.SizeMBAvg AS LogSizeMBAvg ' + @crlf + + N' , bL.SizeMBMin AS LogSizeMBMin ' + @crlf + + N' , bL.SizeMBMax AS LogSizeMBMax ' + @crlf + + N' , bL.CompressedSizeMBAvg AS LogCompressedSizeMBAvg ' + @crlf + + N' , bL.CompressedSizeMBMin AS LogCompressedSizeMBMin ' + @crlf + + N' , bL.CompressedSizeMBMax AS LogCompressedSizeMBMax ' + @crlf + + N' FROM Backups bF ' + @crlf + + N' LEFT OUTER JOIN Backups bD ON bF.database_name = bD.database_name AND bF.database_guid = bD.database_guid AND bD.backup_type = ''I''' + @crlf + + N' LEFT OUTER JOIN Backups bL ON bF.database_name = bL.database_name AND bF.database_guid = bL.database_guid AND bL.backup_type = ''L''' + @crlf + + N' WHERE bF.backup_type = ''D''; ' + @crlf; - /* Look for missing indexes */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 65 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 65) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 65 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Missing Index' AS Finding , - 'https://BrentOzar.com/go/missingindex' AS URL , - ( 'One of the top resource-intensive queries may be dramatically improved by adding an index.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%MissingIndexGroup%'; - END; + IF @Debug = 1 + PRINT @StringToExecute; - /* Look for cursors */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 66 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 66) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 66 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Cursor' AS Finding , - 'https://BrentOzar.com/go/cursor' AS URL , - ( 'One of the top resource-intensive queries is using a cursor.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 187 ) + SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - IF SERVERPROPERTY('IsHadrEnabled') = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 187) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 187 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Endpoints Owned by Users' AS [Finding] , - 'https://BrentOzar.com/go/owners' AS [URL] , - ( 'Endpoint ' + ep.[name] + ' is owned by ' + SUSER_NAME(ep.principal_id) + '. If the endpoint owner login is disabled or not available due to Active Directory problems, the high availability will stop working.' - ) AS [Details] - FROM sys.database_mirroring_endpoints ep - LEFT OUTER JOIN sys.dm_server_services s ON SUSER_NAME(ep.principal_id) = s.service_account - WHERE s.service_account IS NULL AND ep.principal_id <> 1; - END; + SET @StringToExecute += N' + SELECT bs.database_name, bs.database_guid, bs.backup_set_id, bsPrior.backup_set_id AS backup_set_id_prior, + bs.backup_finish_date, bsPrior.backup_finish_date AS backup_finish_date_prior, + DATEDIFF(ss, bsPrior.backup_finish_date, bs.backup_finish_date) AS backup_gap_seconds + INTO #backup_gaps + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs + CROSS APPLY ( + SELECT TOP 1 bs1.backup_set_id, bs1.backup_finish_date + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs1 + WHERE bs.database_name = bs1.database_name + AND bs.database_guid = bs1.database_guid + AND bs.backup_finish_date > bs1.backup_finish_date + AND bs.backup_set_id > bs1.backup_set_id + ORDER BY bs1.backup_finish_date DESC, bs1.backup_set_id DESC + ) bsPrior + WHERE bs.backup_finish_date > @StartTime + + CREATE CLUSTERED INDEX cx_backup_gaps ON #backup_gaps (database_name, database_guid, backup_set_id, backup_finish_date, backup_gap_seconds); + + WITH max_gaps AS ( + SELECT g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, + g.backup_finish_date, MAX(g.backup_gap_seconds) AS max_backup_gap_seconds + FROM #backup_gaps AS g + GROUP BY g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, g.backup_finish_date + ) + UPDATE #Backups + SET RPOWorstCaseMinutes = bg.max_backup_gap_seconds / 60.0 + , RPOWorstCaseBackupSetID = bg.backup_set_id + , RPOWorstCaseBackupSetFinishTime = bg.backup_finish_date + , RPOWorstCaseBackupSetIDPrior = bg.backup_set_id_prior + , RPOWorstCaseBackupSetPriorFinishTime = bg.backup_finish_date_prior + FROM #Backups b + INNER HASH JOIN max_gaps bg ON b.database_name = bg.database_name AND b.database_guid = bg.database_guid + LEFT OUTER HASH JOIN max_gaps bgBigger ON bg.database_name = bgBigger.database_name AND bg.database_guid = bgBigger.database_guid AND bg.max_backup_gap_seconds < bgBigger.max_backup_gap_seconds + WHERE bgBigger.backup_set_id IS NULL; + '; + IF @Debug = 1 + PRINT @StringToExecute; - /*Check for the last good DBCC CHECKDB date */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 68 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT #DBCCs - (ParentObject, - Object, - Field, - Value) - EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); - UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; - - WITH DB2 - AS ( SELECT DISTINCT - Field , - Value , - DbName - FROM #DBCCs - WHERE Field = 'dbi_dbccLastKnownGood' - ) - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 68 AS CheckID , - DB2.DbName AS DatabaseName , - 1 AS PRIORITY , - 'Reliability' AS FindingsGroup , - 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , - 'https://BrentOzar.com/go/checkdb' AS URL , - 'Last successful CHECKDB: ' - + CASE DB2.Value - WHEN '1900-01-01 00:00:00.000' - THEN ' never.' - ELSE DB2.Value - END AS Details - FROM DB2 - WHERE DB2.DbName <> 'tempdb' - AND DB2.DbName NOT IN ( SELECT DISTINCT - DatabaseName - FROM - #SkipChecks - WHERE CheckID IS NULL OR CheckID = 68) - AND DB2.DbName NOT IN ( SELECT name - FROM sys.databases - WHERE is_read_only = 1) - AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, - -14, - CURRENT_TIMESTAMP); - END; + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + RAISERROR('Updating #Backups with worst RPO case queries', 0, 1) WITH NOWAIT; + UPDATE #Backups + SET RPOWorstCaseMoreInfoQuery = @MoreInfoHeader + N'SELECT * ' + @crlf + + N' FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset ' + @crlf + + N' WHERE database_name = ''' + database_name + ''' ' + @crlf + + N' AND database_guid = ''' + CAST(database_guid AS NVARCHAR(50)) + ''' ' + @crlf + + N' AND backup_finish_date >= DATEADD(hh, -2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf + + N' AND backup_finish_date <= DATEADD(hh, 2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf + + N' ORDER BY backup_finish_date;' + + @MoreInfoFooter; - /*Verify that the servername is set */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 70 ) - BEGIN - IF @@SERVERNAME IS NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - '@@Servername Not Set' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details; - END; +/* RTO */ - IF /* @@SERVERNAME IS set */ - (@@SERVERNAME IS NOT NULL - AND - /* not a named instance */ - CHARINDEX('\',CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 - AND - /* not clustered, when computername may be different than the servername */ - SERVERPROPERTY('IsClustered') = 0 - AND - /* @@SERVERNAME is different than the computer name */ - @@SERVERNAME <> CAST(ISNULL(SERVERPROPERTY('ComputerNamePhysicalNetBIOS'),@@SERVERNAME) AS NVARCHAR(128)) ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Configuration' AS FindingsGroup , - '@@Servername Not Correct' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details; - END; +RAISERROR('Gathering RTO information', 0, 1) WITH NOWAIT; - END; - /*Check to see if a failsafe operator has been configured*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 73 ) - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 73) WITH NOWAIT; - - DECLARE @AlertInfo TABLE - ( - FailSafeOperator NVARCHAR(255) , - NotificationMethod INT , - ForwardingServer NVARCHAR(255) , - ForwardingSeverity INT , - PagerToTemplate NVARCHAR(255) , - PagerCCTemplate NVARCHAR(255) , - PagerSubjectTemplate NVARCHAR(255) , - PagerSendSubjectOnly NVARCHAR(255) , - ForwardAlways INT - ); - INSERT INTO @AlertInfo - EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 73 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No failsafe operator configured' AS Finding , - 'https://BrentOzar.com/go/failsafe' AS URL , - ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details - FROM @AlertInfo - WHERE FailSafeOperator IS NULL; - END; + SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; -/*Identify globally enabled trace flags*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - INSERT INTO #TraceStatus - EXEC ( ' DBCC TRACESTATUS(-1) WITH NO_INFOMSGS' - ); - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 74 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'TraceFlag On' AS Finding , - CASE WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN 'https://support.microsoft.com/en-us/kb/3210239' - ELSE'https://www.BrentOzar.com/go/traceflags/' END AS URL , - 'Trace flag ' + - CASE WHEN [T].[TraceFlag] = '2330' THEN ' 2330 enabled globally. Using this trace Flag disables missing index requests' - WHEN [T].[TraceFlag] = '1211' THEN ' 1211 enabled globally. Using this Trace Flag disables lock escalation when you least expect it. No Bueno!' - WHEN [T].[TraceFlag] = '1224' THEN ' 1224 enabled globally. Using this Trace Flag disables lock escalation based on the number of locks being taken. You shouldn''t have done that, Dave.' - WHEN [T].[TraceFlag] = '652' THEN ' 652 enabled globally. Using this Trace Flag disables pre-fetching during index scans. If you hate slow queries, you should turn that off.' - WHEN [T].[TraceFlag] = '661' THEN ' 661 enabled globally. Using this Trace Flag disables ghost record removal. Who you gonna call? No one, turn that thing off.' - WHEN [T].[TraceFlag] = '1806' THEN ' 1806 enabled globally. Using this Trace Flag disables instant file initialization. I question your sanity.' - WHEN [T].[TraceFlag] = '3505' THEN ' 3505 enabled globally. Using this Trace Flag disables Checkpoints. Probably not the wisest idea.' - WHEN [T].[TraceFlag] = '8649' THEN ' 8649 enabled globally. Using this Trace Flag drops cost thresholf for parallelism down to 0. I hope this is a dev server.' - WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN ' 834 is enabled globally. Using this Trace Flag with Columnstore Indexes is not a great idea.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN ' 8017 is enabled globally, which is the default for express edition.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN ' 8017 is enabled globally. Using this Trace Flag disables creation schedulers for all logical processors. Not good.' - ELSE [T].[TraceFlag] + ' is enabled globally.' END - AS Details - FROM #TraceStatus T; - END; + SET @StringToExecute += N' + INSERT INTO #RTORecoveryPoints(database_name, database_guid, log_last_lsn) + SELECT database_name, database_guid, MAX(last_lsn) AS log_last_lsn + FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset bLastLog + WHERE type = ''L'' + AND bLastLog.backup_finish_date >= @StartTime + GROUP BY database_name, database_guid; + '; - /*Check for transaction log file larger than data file */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 75 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 75) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 75 AS CheckID , - DB_NAME(a.database_id) , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Transaction Log Larger than Data File' AS Finding , - 'https://BrentOzar.com/go/biglog' AS URL , - 'The database [' + DB_NAME(a.database_id) - + '] has a ' + CAST((CAST(a.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(20)) + ' GB transaction log file, larger than the total data file sizes. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details - FROM sys.master_files a - WHERE a.type = 1 - AND DB_NAME(a.database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID = 75 OR CheckID IS NULL) - AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ - AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) - FROM sys.master_files b - WHERE a.database_id = b.database_id - AND b.type = 0 - ) - AND a.database_id IN ( - SELECT database_id - FROM sys.databases - WHERE source_database_id IS NULL ); - END; + IF @Debug = 1 + PRINT @StringToExecute; - /*Check for collation conflicts between user databases and tempdb */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 76 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 76) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 76 AS CheckID , - name AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Collation is ' + collation_name AS Finding , - 'https://BrentOzar.com/go/collate' AS URL , - 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details - FROM sys.databases - WHERE name NOT IN ( 'master', 'model', 'msdb') - AND name NOT LIKE 'ReportServer%' - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 76) - AND collation_name <> ( SELECT - collation_name - FROM - sys.databases - WHERE - name = 'tempdb' - ); - END; + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 77 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 77) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 77 AS CheckID , - dSnap.[name] AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Snapshot Online' AS Finding , - 'https://BrentOzar.com/go/snapshot' AS URL , - 'Database [' + dSnap.[name] - + '] is a snapshot of [' - + dOriginal.[name] - + ']. Make sure you have enough drive space to maintain the snapshot as the original database grows.' AS Details - FROM sys.databases dSnap - INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id - AND dSnap.name NOT IN ( - SELECT DISTINCT DatabaseName - FROM #SkipChecks - WHERE CheckID = 77 OR CheckID IS NULL); - END; +/* Find the most recent full backups for those logs */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 79 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 79) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 79 AS CheckID , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days OR Job is enabled AND Job Schedule is enabled - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS FindingsGroup , - 'Shrink Database Job' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - 'In the [' + j.[name] + '] job, step [' - + step.[step_name] - + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS Details - FROM msdb.dbo.sysjobs j - INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE step.command LIKE N'%SHRINKDATABASE%' - OR step.command LIKE N'%SHRINKFILE%'; - END; +RAISERROR('Updating #RTORecoveryPoints', 0, 1) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 81 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 81) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 81 AS CheckID , - 200 AS Priority , - 'Non-Active Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://www.BrentOzar.com/blitz/sp_configure/' AS URL , - ( 'This sp_configure option isn''t running under its set value. Its set value is ' - + CAST(cr.[value] AS VARCHAR(100)) - + ' and its running value is ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '. When someone does a RECONFIGURE or restarts the instance, this setting will start taking effect.' ) AS Details - FROM sys.configurations cr - WHERE cr.value <> cr.value_in_use - AND NOT (cr.name = 'min server memory (MB)' AND cr.value IN (0,16) AND cr.value_in_use IN (0,16)); - END; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 123 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 123) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 123 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Agent Jobs Starting Simultaneously' AS Finding , - 'https://BrentOzar.com/go/busyagent/' AS URL , - ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details - FROM msdb.dbo.sysjobactivity - WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) - GROUP BY start_execution_date HAVING COUNT(*) > 1; - END; + SET @StringToExecute += N' + UPDATE #RTORecoveryPoints + SET log_backup_set_id = bLasted.backup_set_id + ,full_backup_set_id = bLasted.backup_set_id + ,full_last_lsn = bLasted.last_lsn + ,full_backup_set_uuid = bLasted.backup_set_uuid + FROM #RTORecoveryPoints rp + CROSS APPLY ( + SELECT TOP 1 bLog.backup_set_id AS backup_set_id_log, bLastFull.backup_set_id, bLastFull.last_lsn, bLastFull.backup_set_uuid, bLastFull.database_guid, bLastFull.database_name + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog + INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLastFull + ON bLog.database_guid = bLastFull.database_guid + AND bLog.database_name = bLastFull.database_name + AND bLog.first_lsn > bLastFull.last_lsn + AND bLastFull.type = ''D'' + WHERE rp.database_guid = bLog.database_guid + AND rp.database_name = bLog.database_name + ) bLasted + LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLaterFulls ON bLasted.database_guid = bLaterFulls.database_guid AND bLasted.database_name = bLaterFulls.database_name + AND bLasted.last_lsn < bLaterFulls.last_lsn + AND bLaterFulls.first_lsn < bLasted.last_lsn + AND bLaterFulls.type = ''D'' + WHERE bLaterFulls.backup_set_id IS NULL; + '; + IF @Debug = 1 + PRINT @StringToExecute; - IF @CheckServerInfo = 1 - BEGIN + EXEC sys.sp_executesql @StringToExecute; -/*This checks Windows version. It would be better if Microsoft gave everything a separate build number, but whatever.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 172 ) - BEGIN - -- sys.dm_os_host_info includes both Windows and Linux info - IF EXISTS (SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN +/* Add any full backups in the StartDate range that weren't part of the above log backup chain */ - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) +RAISERROR('Add any full backups in the StartDate range that weren''t part of the above log backup chain', 0, 1) WITH NOWAIT; - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Operating System Version' AS [Finding] , - ( CASE WHEN @IsWindowsOperatingSystem = 1 - THEN 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' - ELSE 'https://en.wikipedia.org/wiki/List_of_Linux_distributions' - END - ) AS [URL] , - ( CASE - WHEN [ohi].[host_platform] = 'Linux' THEN 'You''re running the ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ' distribution of ' + CAST([ohi].[host_platform] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' AND [ohi].[host_release] < '6' THEN 'You''re running a really old version: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] >= '6' AND [ohi].[host_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.2' THEN 'You''re running a rather modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - ELSE 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - END - ) AS [Details] - FROM [sys].[dm_os_host_info] [ohi]; - END; - ELSE - BEGIN - -- Otherwise, stick with Windows-only detection + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_windows_info' ) + SET @StringToExecute += N' + INSERT INTO #RTORecoveryPoints(database_name, database_guid, full_backup_set_id, full_last_lsn, full_backup_set_uuid) + SELECT bFull.database_name, bFull.database_guid, bFull.backup_set_id, bFull.last_lsn, bFull.backup_set_uuid + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull + LEFT OUTER JOIN #RTORecoveryPoints rp ON bFull.backup_set_uuid = rp.full_backup_set_uuid + WHERE bFull.type = ''D'' + AND bFull.backup_finish_date IS NOT NULL + AND rp.full_backup_set_uuid IS NULL + AND bFull.backup_finish_date >= @StartTime; + '; - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) + IF @Debug = 1 + PRINT @StringToExecute; - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Windows Version' AS [Finding] , - 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' AS [URL] , - ( CASE - WHEN [owi].[windows_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running a really old version: Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.2' THEN 'You''re running a rather modern version of Windows: Server 2012 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: Server 2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - ELSE 'I have no idea which version of Windows you''re on. Sorry.' - END - ) AS [Details] - FROM [sys].[dm_os_windows_info] [owi]; + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - END; - END; - END; +/* Fill out the most recent log for that full, but before the next full */ -/* -This check hits the dm_os_process_memory system view -to see if locked_page_allocations_kb is > 0, -which could indicate that locked pages in memory is enabled. -*/ -IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 166 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Locked Pages In Memory Enabled' AS [Finding] , - 'https://BrentOzar.com/go/lpim' AS [URL] , - ( 'You currently have ' - + CASE WHEN [dopm].[locked_page_allocations_kb] / 1024. / 1024. > 0 - THEN CAST([dopm].[locked_page_allocations_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([dopm].[locked_page_allocations_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + ' of pages locked in memory.' ) AS [Details] - FROM - [sys].[dm_os_process_memory] AS [dopm] - WHERE - [dopm].[locked_page_allocations_kb] > 0; - END; +RAISERROR('Fill out the most recent log for that full, but before the next full', 0, 1) WITH NOWAIT; - /* Server Info - Locked Pages In Memory Enabled - Check 166 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'sql_memory_model' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 166 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Memory Model Unconventional'' AS Finding , - ''https://BrentOzar.com/go/lpim'' AS URL , - ''Memory Model: '' + CAST(sql_memory_model_desc AS NVARCHAR(100)) - FROM sys.dm_os_sys_info WHERE sql_memory_model <> 1 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.'; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - EXECUTE(@StringToExecute); - END; + SET @StringToExecute += N' + UPDATE rp + SET log_last_lsn = (SELECT MAX(last_lsn) FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog WHERE bLog.first_lsn >= rp.full_last_lsn AND bLog.first_lsn <= rpNextFull.full_last_lsn AND bLog.type = ''L'') + FROM #RTORecoveryPoints rp + INNER JOIN #RTORecoveryPoints rpNextFull ON rp.database_guid = rpNextFull.database_guid AND rp.database_name = rpNextFull.database_name + AND rp.full_last_lsn < rpNextFull.full_last_lsn + LEFT OUTER JOIN #RTORecoveryPoints rpEarlierFull ON rp.database_guid = rpEarlierFull.database_guid AND rp.database_name = rpEarlierFull.database_name + AND rp.full_last_lsn < rpEarlierFull.full_last_lsn + AND rpNextFull.full_last_lsn > rpEarlierFull.full_last_lsn + WHERE rpEarlierFull.full_backup_set_id IS NULL; + '; + IF @Debug = 1 + PRINT @StringToExecute; + EXEC sys.sp_executesql @StringToExecute; - /* - Starting with SQL Server 2014 SP2, Instant File Initialization - is logged in the SQL Server Error Log. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND (@ProductVersionMajor >= 13) OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - INSERT INTO #ErrorLog - EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; +/* Fill out a diff in that range */ - IF @@ROWCOUNT > 0 - INSERT INTO #BlitzResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 193 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Instant File Initialization Enabled' AS [Finding] , - 'https://BrentOzar.com/go/instant' AS [URL] , - 'The service account has the Perform Volume Maintenance Tasks permission.'; - END; +RAISERROR('Fill out a diff in that range', 0, 1) WITH NOWAIT; - /* Server Info - Instant File Initialization Not Enabled - Check 192 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 192 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_server_services' - AND c.name = 'instant_file_initialization_enabled' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 192) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 192 AS CheckID , - 50 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Instant File Initialization Not Enabled'' AS Finding , - ''https://BrentOzar.com/go/instant'' AS URL , - ''Consider enabling IFI for faster restores and data file growths.'' - FROM sys.dm_server_services WHERE instant_file_initialization_enabled <> ''Y'' AND filename LIKE ''%sqlservr.exe%'' 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.'; - - EXECUTE(@StringToExecute); - END; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N' + UPDATE #RTORecoveryPoints + SET diff_last_lsn = (SELECT TOP 1 bDiff.last_lsn FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff + WHERE rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name + AND bDiff.type = ''I'' + AND bDiff.last_lsn < rp.log_last_lsn + AND rp.full_backup_set_uuid = bDiff.differential_base_guid + ORDER BY bDiff.last_lsn DESC) + FROM #RTORecoveryPoints rp + WHERE diff_last_lsn IS NULL; + '; + IF @Debug = 1 + PRINT @StringToExecute; + EXEC sys.sp_executesql @StringToExecute; +/* Get time & size totals for full & diff */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 130 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 130) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 130 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Name' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - @@SERVERNAME AS Details - WHERE @@SERVERNAME IS NOT NULL; - END; +RAISERROR('Get time & size totals for full & diff', 0, 1) WITH NOWAIT; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N' + UPDATE #RTORecoveryPoints + SET full_time_seconds = DATEDIFF(ss,bFull.backup_start_date, bFull.backup_finish_date) + , full_file_size_mb = bFull.backup_size / 1048576.0 + , diff_backup_set_id = bDiff.backup_set_id + , diff_time_seconds = DATEDIFF(ss,bDiff.backup_start_date, bDiff.backup_finish_date) + , diff_file_size_mb = bDiff.backup_size / 1048576.0 + FROM #RTORecoveryPoints rp + INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull ON rp.database_guid = bFull.database_guid AND rp.database_name = bFull.database_name AND rp.full_last_lsn = bFull.last_lsn + LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff ON rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name AND rp.diff_last_lsn = bDiff.last_lsn AND bDiff.last_lsn IS NOT NULL; + '; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 83 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 83) WITH NOWAIT; - - -- DATETIMEOFFSET and DATETIME have different minimum values, so there's - -- a small workaround here to force 1753-01-01 if the minimum is detected - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 83 AS CheckID , - 250 AS Priority , - ''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 + ''.'' - FROM sys.dm_server_services 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.'; - - EXECUTE(@StringToExecute); - END; - END; + IF @Debug = 1 + PRINT @StringToExecute; + + EXEC sys.sp_executesql @StringToExecute; - /* Check 84 - SQL Server 2012 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 84 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_kb' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_kb / 1024.0 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - /* Check 84 - SQL Server 2008 */ - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_in_bytes' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_in_bytes / 1024.0 / 1024 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - END; +/* Get time & size totals for logs */ +RAISERROR('Get time & size totals for logs', 0, 1) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 85 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 85) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 85 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Service' AS Finding , - '' AS URL , - N'Version: ' - + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) - + N'. Patch Level: ' - + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) - + N'. Edition: ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + N'. AlwaysOn Enabled: ' - + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), - 0) AS VARCHAR(100)) - + N'. AlwaysOn Mgr Status: ' - + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), - 0) AS VARCHAR(100)); - END; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N' + WITH LogTotals AS ( + SELECT rp.id, log_time_seconds = SUM(DATEDIFF(ss,bLog.backup_start_date, bLog.backup_finish_date)) + , log_file_size = SUM(bLog.backup_size) + , SUM(1) AS log_backups + FROM #RTORecoveryPoints rp + INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog ON rp.database_guid = bLog.database_guid AND rp.database_name = bLog.database_name AND bLog.type = ''L'' + AND bLog.first_lsn > COALESCE(rp.diff_last_lsn, rp.full_last_lsn) + AND bLog.first_lsn <= rp.log_last_lsn + GROUP BY rp.id + ) + UPDATE #RTORecoveryPoints + SET log_time_seconds = lt.log_time_seconds + , log_file_size_mb = lt.log_file_size / 1048576.0 + , log_backups = lt.log_backups + FROM #RTORecoveryPoints rp + INNER JOIN LogTotals lt ON rp.id = lt.id; + '; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 88 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 88) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 88 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Last Restart' AS Finding , - '' AS URL , - CAST(create_date AS VARCHAR(100)) - FROM sys.databases - WHERE database_id = 2; - END; + IF @Debug = 1 + PRINT @StringToExecute; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 91 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 91) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 91 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Last Restart' AS Finding , - '' AS URL , - CAST(DATEADD(SECOND, (ms_ticks/1000)*(-1), GETDATE()) AS nvarchar(25)) - FROM sys.dm_os_sys_info; - END; + EXEC sys.sp_executesql @StringToExecute; +RAISERROR('Gathering RTO worst cases', 0, 1) WITH NOWAIT; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 92 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 92) WITH NOWAIT; - - INSERT INTO #driveInfo - ( drive, SIZE ) - EXEC master..xp_fixeddrives; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 92 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Drive ' + i.drive + ' Space' AS Finding , - '' AS URL , - CAST(i.SIZE AS VARCHAR(30)) - + 'MB free on ' + i.drive - + ' drive' AS Details - FROM #driveInfo AS i; - DROP TABLE #driveInfo; - END; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N' + WITH WorstCases AS ( + SELECT rp.* + FROM #RTORecoveryPoints rp + LEFT OUTER JOIN #RTORecoveryPoints rpNewer + ON rp.database_guid = rpNewer.database_guid + AND rp.database_name = rpNewer.database_name + AND rp.full_last_lsn < rpNewer.full_last_lsn + AND rpNewer.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) + WHERE rp.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) + /* OR rp.rto_worst_case_time_seconds = (SELECT TOP 1 rto_worst_case_time_seconds FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_time_seconds DESC) */ + AND rpNewer.database_guid IS NULL + ) + UPDATE #Backups + SET RTOWorstCaseMinutes = + /* Fulls */ + (CASE WHEN @RestoreSpeedFullMBps IS NULL + THEN wc.full_time_seconds / 60.0 + ELSE @RestoreSpeedFullMBps / wc.full_file_size_mb + END) - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 103 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'virtual_machine_type_desc' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 103) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 103 AS CheckID, - 250 AS Priority, - ''Server Info'' AS FindingsGroup, - ''Virtual Server'' AS Finding, - ''https://BrentOzar.com/go/virtual'' AS URL, - ''Type: ('' + virtual_machine_type_desc + '')'' AS Details - FROM sys.dm_os_sys_info - WHERE virtual_machine_type <> 0 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.'; - - EXECUTE(@StringToExecute); - END; + /* Diffs, which might not have been taken */ + + (CASE WHEN @RestoreSpeedDiffMBps IS NOT NULL AND wc.diff_file_size_mb IS NOT NULL + THEN @RestoreSpeedDiffMBps / wc.diff_file_size_mb + ELSE COALESCE(wc.diff_time_seconds,0) / 60.0 + END) - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 114 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_os_memory_nodes' ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_nodes' - AND c.name = 'processor_group' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 114) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 114 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware - NUMA Config'' AS Finding , - '''' AS URL , - ''Node: '' + CAST(n.node_id AS NVARCHAR(10)) + '' State: '' + node_state_desc - + '' Online schedulers: '' + CAST(n.online_scheduler_count AS NVARCHAR(10)) + '' Offline schedulers: '' + CAST(oac.offline_schedulers AS VARCHAR(100)) + '' Processor Group: '' + CAST(n.processor_group AS NVARCHAR(10)) - + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - OUTER APPLY (SELECT - COUNT(*) AS [offline_schedulers] - FROM sys.dm_os_schedulers dos - WHERE n.node_id = dos.parent_node_id - AND dos.status = ''VISIBLE OFFLINE'' - ) oac - WHERE n.node_state_desc NOT LIKE ''%DAC%'' - ORDER BY n.node_id 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.'; - - EXECUTE(@StringToExecute); - END; + /* Logs, which might not have been taken */ + + (CASE WHEN @RestoreSpeedLogMBps IS NOT NULL AND wc.log_file_size_mb IS NOT NULL + THEN @RestoreSpeedLogMBps / wc.log_file_size_mb + ELSE COALESCE(wc.log_time_seconds,0) / 60.0 + END) + , RTOWorstCaseBackupFileSizeMB = wc.rto_worst_case_size_mb + FROM #Backups b + INNER JOIN WorstCases wc + ON b.database_guid = wc.database_guid + AND b.database_name = wc.database_name; + '; + IF @Debug = 1 + PRINT @StringToExecute; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') - BEGIN + EXEC sys.sp_executesql @StringToExecute, N'@RestoreSpeedFullMBps INT, @RestoreSpeedDiffMBps INT, @RestoreSpeedLogMBps INT', @RestoreSpeedFullMBps, @RestoreSpeedDiffMBps, @RestoreSpeedLogMBps; - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 106) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 106 AS CheckID - ,250 AS Priority - ,'Server Info' AS FindingsGroup - ,'Default Trace Contents' AS Finding - ,'https://BrentOzar.com/go/trace' AS URL - ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as VARCHAR(30))+' hours of data' - +' between '+cast(Min(StartTime) as VARCHAR(30))+' and '+cast(GETDATE()as VARCHAR(30)) - +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) - ) as Details - FROM ::fn_trace_gettable( @base_tracefilename, default ) - WHERE EventClass BETWEEN 65500 and 65600; - END; /* CheckID 106 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 152 ) - BEGIN - IF EXISTS (SELECT * FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 - AND i.wait_type IS NULL) - BEGIN - /* Check for waits that have had more than 10% of the server's wait time */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 152) WITH NOWAIT; - - WITH os(wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) - AS - (SELECT ws.wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms - FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE i.wait_type IS NULL - AND wait_time_ms > .1 * @CpuMsSinceWaitsCleared - AND waiting_tasks_count > 0) - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 9 - 152 AS CheckID - ,240 AS Priority - ,'Wait Stats' AS FindingsGroup - , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding - ,'https://BrentOzar.com/go/waits' AS URL - , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' hours of waits, ' + - CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + - /* CAST(CAST( - 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER () ) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% of waits, ' + */ - CAST(CAST( - 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER ()) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + - CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + - CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 - THEN - CAST( - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) - AS NUMERIC(18,1)) - ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' - FROM os - ORDER BY SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) DESC; - END; /* IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND waiting_tasks_count > 0) */ +/*Populating Recoverability*/ - /* If no waits were found, add a note about that */ - IF NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (107, 108, 109, 121, 152, 162)) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 153) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://BrentOzar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); - END; - END; /* CheckID 152 */ - END; /* IF @CheckServerInfo = 1 */ - END; /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ + /*Get distinct list of databases*/ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N' + SELECT DISTINCT b.database_name, database_guid + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b;' - /* Delete priorites they wanted to skip. */ - IF @IgnorePrioritiesAbove IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] > @IgnorePrioritiesAbove AND CheckID <> -1; + IF @Debug = 1 + PRINT @StringToExecute; - IF @IgnorePrioritiesBelow IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] < @IgnorePrioritiesBelow AND CheckID <> -1; + INSERT #Recoverability ( DatabaseName, DatabaseGUID ) + EXEC sys.sp_executesql @StringToExecute; - /* Delete checks they wanted to skip. */ - IF @SkipChecksTable IS NOT NULL - BEGIN - DELETE FROM #BlitzResults - WHERE DatabaseName IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE FROM #BlitzResults - WHERE CheckID IN ( SELECT CheckID - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE r FROM #BlitzResults r - INNER JOIN #SkipChecks c ON r.DatabaseName = c.DatabaseName and r.CheckID = c.CheckID - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName')); - END; - - /* Add summary mode */ - IF @SummaryMode > 0 - BEGIN - UPDATE #BlitzResults - SET Finding = br.Finding + ' (' + CAST(brTotals.recs AS NVARCHAR(20)) + ')' - FROM #BlitzResults br - INNER JOIN (SELECT FindingsGroup, Finding, Priority, COUNT(*) AS recs FROM #BlitzResults GROUP BY FindingsGroup, Finding, Priority) brTotals ON br.FindingsGroup = brTotals.FindingsGroup AND br.Finding = brTotals.Finding AND br.Priority = brTotals.Priority - WHERE brTotals.recs > 1; - DELETE br - FROM #BlitzResults br - WHERE EXISTS (SELECT * FROM #BlitzResults brLower WHERE br.FindingsGroup = brLower.FindingsGroup AND br.Finding = brLower.Finding AND br.Priority = brLower.Priority AND br.ID > brLower.ID); + /*Find most recent recovery model, backup size, and backup date*/ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - END; + SET @StringToExecute += N' + UPDATE r + SET r.LastBackupRecoveryModel = ca.recovery_model, + r.LastFullBackupSizeMB = ca.compressed_backup_size, + r.LastFullBackupDate = ca.backup_finish_date + FROM #Recoverability r + CROSS APPLY ( + SELECT TOP 1 b.recovery_model, (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''D'' + AND b.backup_finish_date > @StartTime + ORDER BY b.backup_finish_date DESC + ) ca;' - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org' , - 'We hope you found this tool useful.' - ); + IF @Debug = 1 + PRINT @StringToExecute; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - ) - VALUES ( -1 , - 0 , - 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - 'SQL Server First Responder Kit' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' + /*Find first backup size and date*/ + SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - ); + SET @StringToExecute += N' + UPDATE r + SET r.FirstFullBackupSizeMB = ca.compressed_backup_size, + r.FirstFullBackupDate = ca.backup_finish_date + FROM #Recoverability r + CROSS APPLY ( + SELECT TOP 1 (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''D'' + AND b.backup_finish_date > @StartTime + ORDER BY b.backup_finish_date ASC + ) ca;' - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details + IF @Debug = 1 + PRINT @StringToExecute; - ) - SELECT 156 , - 254 , - 'Rundate' , - GETDATE() , - 'http://FirstResponderKit.org/' , - 'Captain''s log: stardate something and something...'; - - IF @EmailRecipients IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Sending an email.', 0, 1) WITH NOWAIT; - - /* Database mail won't work off a local temp table. I'm not happy about this hacky workaround either. */ - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - SELECT * INTO ##BlitzResults FROM #BlitzResults; - SET @query_result_separator = char(9); - SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup, Finding, Details; SET NOCOUNT OFF;'; - SET @EmailSubject = 'sp_Blitz Results for ' + @@SERVERNAME; - SET @EmailBody = 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. http://FirstResponderKit.org'; - IF @EmailProfile IS NULL - EXEC msdb.dbo.sp_send_dbmail - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - ELSE - EXEC msdb.dbo.sp_send_dbmail - @profile_name = @EmailProfile, - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - END; + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT; - DECLARE @ValidOutputLocation BIT; - DECLARE @LinkedServerDBCheck NVARCHAR(2000); - DECLARE @ValidLinkedServerDB INT; - DECLARE @tmpdbchk table (cnt int); - IF @OutputServerName IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; - - IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) - BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); - IF (@ValidLinkedServerDB > 0) - BEGIN - SET @ValidOutputServer = 1; - SET @ValidOutputLocation = 1; - END; - ELSE - RAISERROR('The specified database was not found on the output server', 16, 0); - END; - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0); - END; - END; - ELSE - BEGIN - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1; - END; - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0); - END; - ELSE - BEGIN - SET @ValidOutputLocation = 0; - END; - END; - /* @OutputTableName lets us export the results to a permanent table */ - IF @ValidOutputLocation = 1 - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));'; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); - EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END; - ELSE - BEGIN - EXEC(@StringToExecute); - END; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputServerName + '.' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputServerName + '.' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; + /*Find average backup throughputs for full, diff, and log*/ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - EXEC(@StringToExecute); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - IF @ValidOutputServer = 1 - BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' - + 'CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); - END; + SET @StringToExecute += N' + UPDATE r + SET r.AvgFullBackupThroughputMB = ca_full.AvgFullSpeed, + r.AvgDiffBackupThroughputMB = ca_diff.AvgDiffSpeed, + r.AvgLogBackupThroughputMB = ca_log.AvgLogSpeed, + r.AvgFullBackupDurationSeconds = AvgFullDuration, + r.AvgDiffBackupDurationSeconds = AvgDiffDuration, + r.AvgLogBackupDurationSeconds = AvgLogDuration + FROM #Recoverability AS r + OUTER APPLY ( + SELECT b.database_name, + AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgFullSpeed, + AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgFullDuration + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''D'' + AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) ca_full + OUTER APPLY ( + SELECT b.database_name, + AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgDiffSpeed, + AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgDiffDuration + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''I'' + AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) ca_diff + OUTER APPLY ( + SELECT b.database_name, + AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgLogSpeed, + AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgLogDuration + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''L'' + AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) ca_log;' + IF @Debug = 1 + PRINT @StringToExecute; - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - IF @OutputType = 'COUNT' - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzResults; - END; - ELSE - IF @OutputType IN ( 'CSV', 'RSV' ) - BEGIN - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputXMLasNVARCHAR = 1 AND @OutputType <> 'NONE' - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan, - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputType = 'MARKDOWN' - BEGIN - WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * - FROM #BlitzResults - WHERE Priority > 0 AND Priority < 255 AND FindingsGroup IS NOT NULL AND Finding IS NOT NULL - AND FindingsGroup <> 'Security' /* Specifically excluding security checks for public exports */) - SELECT - CASE - WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf - ELSE N'' - END - + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END - ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END - END + @crlf - FROM Results r - LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 - LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 - ORDER BY r.rownum FOR XML PATH(N''); - END; - ELSE IF @OutputType <> 'NONE' - BEGIN - /* --TOURSTOP05-- */ - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - [QueryPlan] , - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; + /*Find max and avg diff and log sizes*/ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - DROP TABLE #BlitzResults; + SET @StringToExecute += N' + UPDATE r + SET r.AvgFullSizeMB = fulls.avg_full_size, + r.AvgDiffSizeMB = diffs.avg_diff_size, + r.AvgLogSizeMB = logs.avg_log_size + FROM #Recoverability AS r + OUTER APPLY ( + SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_full_size + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''D'' + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) AS fulls + OUTER APPLY ( + SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_diff_size + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''I'' + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) AS diffs + OUTER APPLY ( + SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_log_size + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE r.DatabaseName = b.database_name + AND r.DatabaseGUID = b.database_guid + AND b.type = ''L'' + AND b.backup_finish_date > @StartTime + GROUP BY b.database_name + ) AS logs;' - IF @OutputProcedureCache = 1 - AND @CheckProcedureCache = 1 - SELECT TOP 20 - total_worker_time / execution_count AS AvgCPU , - total_worker_time AS TotalCPU , - CAST(ROUND(100.00 * total_worker_time - / ( SELECT SUM(total_worker_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentCPU , - total_elapsed_time / execution_count AS AvgDuration , - total_elapsed_time AS TotalDuration , - CAST(ROUND(100.00 * total_elapsed_time - / ( SELECT SUM(total_elapsed_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - CAST(ROUND(100.00 * total_logical_reads - / ( SELECT SUM(total_logical_reads) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentReads , - execution_count , - CAST(ROUND(100.00 * execution_count - / ( SELECT SUM(execution_count) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentExecutions , - CASE WHEN DATEDIFF(mi, creation_time, - qs.last_execution_time) = 0 THEN 0 - ELSE CAST(( 1.00 * execution_count / DATEDIFF(mi, - creation_time, - qs.last_execution_time) ) AS MONEY) - END AS executions_per_minute , - qs.creation_time AS plan_creation_time , - qs.last_execution_time , - text , - text_filtered , - query_plan , - query_plan_filtered , - sql_handle , - query_hash , - plan_handle , - query_plan_hash - FROM #dm_exec_query_stats qs - ORDER BY CASE UPPER(@CheckProcedureCacheFilter) - WHEN 'CPU' THEN total_worker_time - WHEN 'READS' THEN total_logical_reads - WHEN 'EXECCOUNT' THEN execution_count - WHEN 'DURATION' THEN total_elapsed_time - ELSE total_worker_time - END DESC; + IF @Debug = 1 + PRINT @StringToExecute; - END; /* ELSE -- IF @OutputType = 'SCHEMA' */ + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + +/*Trending - only works if backupfile is populated, which means in msdb */ +IF @MSDBName = N'msdb' +BEGIN + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' --+ @crlf; - SET NOCOUNT OFF; -GO + SET @StringToExecute += N' + SELECT p.DatabaseName, + p.DatabaseGUID, + p.[0], + p.[-1], + p.[-2], + p.[-3], + p.[-4], + p.[-5], + p.[-6], + p.[-7], + p.[-8], + p.[-9], + p.[-10], + p.[-11], + p.[-12] + FROM ( SELECT b.database_name AS DatabaseName, + b.database_guid AS DatabaseGUID, + DATEDIFF(MONTH, @StartTime, b.backup_start_date) AS MonthsAgo , + CONVERT(DECIMAL(18, 2), AVG(bf.file_size / 1048576.0)) AS AvgSizeMB + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupfile AS bf + ON b.backup_set_id = bf.backup_set_id + WHERE b.database_name NOT IN ( ''master'', ''msdb'', ''model'', ''tempdb'' ) + AND bf.file_type = ''D'' + AND b.backup_start_date >= DATEADD(YEAR, -1, @StartTime) + AND b.backup_start_date <= SYSDATETIME() + GROUP BY b.database_name, + b.database_guid, + DATEDIFF(mm, @StartTime, b.backup_start_date) + ) AS bckstat PIVOT ( SUM(bckstat.AvgSizeMB) FOR bckstat.MonthsAgo IN ( [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) ) AS p + ORDER BY p.DatabaseName; + ' -/* ---Sample execution call with the most common parameters: -EXEC [dbo].[sp_Blitz] - @CheckUserDatabaseObjects = 1 , - @CheckProcedureCache = 0 , - @OutputType = 'TABLE' , - @OutputProcedureCache = 0 , - @CheckProcedureCacheFilter = NULL, - @CheckServerInfo = 1 -*/ -IF OBJECT_ID('dbo.sp_BlitzBackups') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzBackups AS RETURN 0;'); -GO -ALTER PROCEDURE [dbo].[sp_BlitzBackups] - @Help TINYINT = 0 , - @HoursBack INT = 168, - @MSDBName NVARCHAR(256) = 'msdb', - @AGName NVARCHAR(256) = NULL, - @RestoreSpeedFullMBps INT = NULL, - @RestoreSpeedDiffMBps INT = NULL, - @RestoreSpeedLogMBps INT = NULL, - @Debug TINYINT = 0, - @PushBackupHistoryToListener BIT = 0, - @WriteBackupsToListenerName NVARCHAR(256) = NULL, - @WriteBackupsToDatabaseName NVARCHAR(256) = NULL, - @WriteBackupsLastHours INT = 168, - @VersionDate DATE = NULL OUTPUT -WITH RECOMPILE -AS - BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; + IF @Debug = 1 + PRINT @StringToExecute; - IF @Help = 1 PRINT ' - /* - sp_BlitzBackups from http://FirstResponderKit.org - - This script checks your backups to see how much data you might lose when - this server fails, and how long it might take to recover. + INSERT #Trending ( DatabaseName, DatabaseGUID, [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) + EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - 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. +END - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. +/*End Trending*/ - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) +/*End populating Recoverability*/ - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ +RAISERROR('Returning data', 0, 1) WITH NOWAIT; + + SELECT b.* + FROM #Backups AS b + ORDER BY b.database_name; + SELECT r.*, + t.[0], t.[-1], t.[-2], t.[-3], t.[-4], t.[-5], t.[-6], t.[-7], t.[-8], t.[-9], t.[-10], t.[-11], t.[-12] + FROM #Recoverability AS r + LEFT JOIN #Trending t + ON r.DatabaseName = t.DatabaseName + AND r.DatabaseGUID = t.DatabaseGUID + WHERE r.LastBackupRecoveryModel IS NOT NULL + ORDER BY r.DatabaseName - Parameter explanations: - @HoursBack INT = 168 How many hours of history to examine, back from now. - You can check just the last 24 hours of backups, for example. - @MSDBName NVARCHAR(255) You can restore MSDB from different servers and check them - centrally. Also useful if you create a DBA utility database - and merge data from several servers in an AG into one DB. - @RestoreSpeedFullMBps INT By default, we use the backup speed from MSDB to guesstimate - how fast your restores will go. If you have done performance - tuning and testing of your backups (or if they horribly go even - slower in your DR environment, and you want to account for - that), then you can pass in different numbers here. - @RestoreSpeedDiffMBps INT See above. - @RestoreSpeedLogMBps INT See above. +RAISERROR('Rules analysis starting', 0, 1) WITH NOWAIT; - For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ +/*Looking for out of band backups by finding most common backup operator user_name and noting backups taken by other user_names*/ - MIT License + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N' + WITH common_people AS ( + SELECT TOP 1 b.user_name, COUNT_BIG(*) AS Records + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + GROUP BY b.user_name + ORDER BY Records DESC + ) + SELECT + 1 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Non-Agent backups taken'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has been backed up by '' + QUOTENAME(b.user_name) + '' '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.user_name NOT LIKE ''%Agent%'' AND b.user_name NOT LIKE ''%AGENT%'' + AND NOT EXISTS ( + SELECT 1 + FROM common_people AS cp + WHERE cp.user_name = b.user_name + ) + GROUP BY b.database_name, b.user_name + HAVING COUNT(*) > 1;' + @crlf; + + IF @Debug = 1 + PRINT @StringToExecute; + + INSERT #Warnings (CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; - Copyright (c) 2017 Brent Ozar Unlimited + /*Looking for compatibility level changing. Only looking for databases that have changed more than twice (It''s possible someone may have changed up, had CE problems, and then changed back)*/ - 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: + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + SET @StringToExecute += N'SELECT + 2 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Compatibility level changing'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has changed compatibility levels '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.compatibility_level)) + '' times.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + GROUP BY b.database_name + HAVING COUNT(DISTINCT b.compatibility_level) > 2;' + @crlf; - 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. + IF @Debug = 1 + PRINT @StringToExecute; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*Looking for password protected backups. This hasn''t been a popular option ever, and was largely replaced by encrypted backups, but it''s simple to check for.*/ + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N'SELECT + 3 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Password backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has been backed up with a password '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times. Who has the password?'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.is_password_protected = 1 + GROUP BY b.database_name;' + @crlf; - */'; -ELSE -BEGIN -DECLARE @StringToExecute NVARCHAR(MAX) = N'', - @InnerStringToExecute NVARCHAR(MAX) = N'', - @ProductVersion NVARCHAR(128), - @ProductVersionMajor DECIMAL(10, 2), - @ProductVersionMinor DECIMAL(10, 2), - @StartTime DATETIME2, @ResultText NVARCHAR(MAX), - @crlf NVARCHAR(2), - @MoreInfoHeader NVARCHAR(100), - @MoreInfoFooter NVARCHAR(100); + IF @Debug = 1 + PRINT @StringToExecute; -IF @HoursBack > 0 - SET @HoursBack = @HoursBack * -1; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*Looking for snapshot backups. There are legit reasons for these, but we should flag them so the questions get asked. What questions? Good question.*/ -IF @WriteBackupsLastHours > 0 - SET @WriteBackupsLastHours = @WriteBackupsLastHours * -1; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; -SELECT @crlf = NCHAR(13) + NCHAR(10), - @StartTime = DATEADD(hh, @HoursBack, GETDATE()), - @MoreInfoHeader = N''; + SET @StringToExecute += N'SELECT + 4 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Snapshot backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' snapshot backups. This message is purely informational.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.is_snapshot = 1 + GROUP BY b.database_name;' + @crlf; -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); + IF @Debug = 1 + PRINT @StringToExecute; -CREATE TABLE #Backups -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - RPOWorstCaseMinutes DECIMAL(18, 1), - RTOWorstCaseMinutes DECIMAL(18, 1), - RPOWorstCaseBackupSetID INT, - RPOWorstCaseBackupSetFinishTime DATETIME, - RPOWorstCaseBackupSetIDPrior INT, - RPOWorstCaseBackupSetPriorFinishTime DATETIME, - RPOWorstCaseMoreInfoQuery XML, - RTOWorstCaseBackupFileSizeMB DECIMAL(18, 2), - RTOWorstCaseMoreInfoQuery XML, - FullMBpsAvg DECIMAL(18, 2), - FullMBpsMin DECIMAL(18, 2), - FullMBpsMax DECIMAL(18, 2), - FullSizeMBAvg DECIMAL(18, 2), - FullSizeMBMin DECIMAL(18, 2), - FullSizeMBMax DECIMAL(18, 2), - FullCompressedSizeMBAvg DECIMAL(18, 2), - FullCompressedSizeMBMin DECIMAL(18, 2), - FullCompressedSizeMBMax DECIMAL(18, 2), - DiffMBpsAvg DECIMAL(18, 2), - DiffMBpsMin DECIMAL(18, 2), - DiffMBpsMax DECIMAL(18, 2), - DiffSizeMBAvg DECIMAL(18, 2), - DiffSizeMBMin DECIMAL(18, 2), - DiffSizeMBMax DECIMAL(18, 2), - DiffCompressedSizeMBAvg DECIMAL(18, 2), - DiffCompressedSizeMBMin DECIMAL(18, 2), - DiffCompressedSizeMBMax DECIMAL(18, 2), - LogMBpsAvg DECIMAL(18, 2), - LogMBpsMin DECIMAL(18, 2), - LogMBpsMax DECIMAL(18, 2), - LogSizeMBAvg DECIMAL(18, 2), - LogSizeMBMin DECIMAL(18, 2), - LogSizeMBMax DECIMAL(18, 2), - LogCompressedSizeMBAvg DECIMAL(18, 2), - LogCompressedSizeMBMin DECIMAL(18, 2), - LogCompressedSizeMBMax DECIMAL(18, 2) -); - -CREATE TABLE #RTORecoveryPoints -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - rto_worst_case_size_mb AS - ( COALESCE(log_file_size_mb, 0) + COALESCE(diff_file_size_mb, 0) + COALESCE(full_file_size_mb, 0)), - rto_worst_case_time_seconds AS - ( COALESCE(log_time_seconds, 0) + COALESCE(diff_time_seconds, 0) + COALESCE(full_time_seconds, 0)), - full_backup_set_id INT, - full_last_lsn NUMERIC(25, 0), - full_backup_set_uuid UNIQUEIDENTIFIER, - full_time_seconds BIGINT, - full_file_size_mb DECIMAL(18, 2), - diff_backup_set_id INT, - diff_last_lsn NUMERIC(25, 0), - diff_time_seconds BIGINT, - diff_file_size_mb DECIMAL(18, 2), - log_backup_set_id INT, - log_last_lsn NUMERIC(25, 0), - log_time_seconds BIGINT, - log_file_size_mb DECIMAL(18, 2), - log_backups INT -); + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*It''s fine to take backups of read only databases, but it''s not always necessary (there''s no new data, after all).*/ -CREATE TABLE #Recoverability - ( - Id INT IDENTITY , - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - LastBackupRecoveryModel NVARCHAR(60), - FirstFullBackupSizeMB DECIMAL (18,2), - FirstFullBackupDate DATETIME, - LastFullBackupSizeMB DECIMAL (18,2), - LastFullBackupDate DATETIME, - AvgFullBackupThroughputMB DECIMAL (18,2), - AvgFullBackupDurationSeconds INT, - AvgDiffBackupThroughputMB DECIMAL (18,2), - AvgDiffBackupDurationSeconds INT, - AvgLogBackupThroughputMB DECIMAL (18,2), - AvgLogBackupDurationSeconds INT, - AvgFullSizeMB DECIMAL (18,2), - AvgDiffSizeMB DECIMAL (18,2), - AvgLogSizeMB DECIMAL (18,2), - IsBigDiff AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgDiffSizeMB * 100.) / AvgFullSizeMB >= 40.)) THEN 1 ELSE 0 END, - IsBigLog AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgLogSizeMB * 100.) / AvgFullSizeMB >= 20.)) THEN 1 ELSE 0 END - ); + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; -CREATE TABLE #Trending -( - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - [0] DECIMAL(18, 2), - [-1] DECIMAL(18, 2), - [-2] DECIMAL(18, 2), - [-3] DECIMAL(18, 2), - [-4] DECIMAL(18, 2), - [-5] DECIMAL(18, 2), - [-6] DECIMAL(18, 2), - [-7] DECIMAL(18, 2), - [-8] DECIMAL(18, 2), - [-9] DECIMAL(18, 2), - [-10] DECIMAL(18, 2), - [-11] DECIMAL(18, 2), - [-12] DECIMAL(18, 2) -); + SET @StringToExecute += N'SELECT + 5 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Read only state backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in a read-only state. This can be normal if it''''s a secondary, but a bit odd otherwise.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.is_readonly = 1 + GROUP BY b.database_name;' + @crlf; + IF @Debug = 1 + PRINT @StringToExecute; -CREATE TABLE #Warnings -( - Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckId INT, - Priority INT, - DatabaseName VARCHAR(128), - Finding VARCHAR(256), - Warning VARCHAR(8000) -); + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*So, I''ve come across people who think they need to change their database to single user mode to take a backup. Or that doing that will help something. I just need to know, here.*/ -IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = @MSDBName) - BEGIN - RAISERROR('@MSDBName was specified, but the database does not exist.', 0, 1) WITH NOWAIT; - RETURN; - END + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; -IF @PushBackupHistoryToListener = 1 -GOTO PushBackupHistoryToListener + SET @StringToExecute += N'SELECT + 6 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Single user mode backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in single-user mode. This is really weird! Make sure your backup process doesn''''t include a mode change anywhere.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.is_single_user = 1 + GROUP BY b.database_name;' + @crlf; + IF @Debug = 1 + PRINT @StringToExecute; - RAISERROR('Inserting to #Backups', 0, 1) WITH NOWAIT; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*C''mon, it''s 2017. Take your backups with CHECKSUMS, people.*/ SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N'WITH Backups AS (SELECT bs.database_name, bs.database_guid, bs.type AS backup_type ' + @crlf - + ' , MBpsAvg = CAST(AVG(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMin = CAST(MIN(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMax = CAST(MAX(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , SizeMBAvg = AVG(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMin = MIN(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMax = MAX(backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBAvg = AVG(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMin = MIN(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMax = MAX(compressed_backup_size / 1048576.0) ' + @crlf; + SET @StringToExecute += N'SELECT + 7 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''No CHECKSUMS'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times without CHECKSUMS in the past 30 days. CHECKSUMS can help alert you to corruption errors.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.has_backup_checksums = 0 + AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) + GROUP BY b.database_name;' + @crlf; + IF @Debug = 1 + PRINT @StringToExecute; - SET @StringToExecute += N' FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bs ' + @crlf - + N' WHERE bs.backup_finish_date >= @StartTime AND bs.is_damaged = 0 ' + @crlf - + N' GROUP BY bs.database_name, bs.database_guid, bs.type)' + @crlf; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*Damaged is a Black Flag album. You don''t want your backups to be like a Black Flag album. */ - SET @StringToExecute += + N'INSERT INTO #Backups(database_name, database_guid, ' + @crlf - + N' FullMBpsAvg, FullMBpsMin, FullMBpsMax, FullSizeMBAvg, FullSizeMBMin, FullSizeMBMax, FullCompressedSizeMBAvg, FullCompressedSizeMBMin, FullCompressedSizeMBMax, ' + @crlf - + N' DiffMBpsAvg, DiffMBpsMin, DiffMBpsMax, DiffSizeMBAvg, DiffSizeMBMin, DiffSizeMBMax, DiffCompressedSizeMBAvg, DiffCompressedSizeMBMin, DiffCompressedSizeMBMax, ' + @crlf - + N' LogMBpsAvg, LogMBpsMin, LogMBpsMax, LogSizeMBAvg, LogSizeMBMin, LogSizeMBMax, LogCompressedSizeMBAvg, LogCompressedSizeMBMin, LogCompressedSizeMBMax ) ' + @crlf - + N'SELECT bF.database_name, bF.database_guid ' + @crlf - + N' , bF.MBpsAvg AS FullMBpsAvg ' + @crlf - + N' , bF.MBpsMin AS FullMBpsMin ' + @crlf - + N' , bF.MBpsMax AS FullMBpsMax ' + @crlf - + N' , bF.SizeMBAvg AS FullSizeMBAvg ' + @crlf - + N' , bF.SizeMBMin AS FullSizeMBMin ' + @crlf - + N' , bF.SizeMBMax AS FullSizeMBMax ' + @crlf - + N' , bF.CompressedSizeMBAvg AS FullCompressedSizeMBAvg ' + @crlf - + N' , bF.CompressedSizeMBMin AS FullCompressedSizeMBMin ' + @crlf - + N' , bF.CompressedSizeMBMax AS FullCompressedSizeMBMax ' + @crlf - + N' , bD.MBpsAvg AS DiffMBpsAvg ' + @crlf - + N' , bD.MBpsMin AS DiffMBpsMin ' + @crlf - + N' , bD.MBpsMax AS DiffMBpsMax ' + @crlf - + N' , bD.SizeMBAvg AS DiffSizeMBAvg ' + @crlf - + N' , bD.SizeMBMin AS DiffSizeMBMin ' + @crlf - + N' , bD.SizeMBMax AS DiffSizeMBMax ' + @crlf - + N' , bD.CompressedSizeMBAvg AS DiffCompressedSizeMBAvg ' + @crlf - + N' , bD.CompressedSizeMBMin AS DiffCompressedSizeMBMin ' + @crlf - + N' , bD.CompressedSizeMBMax AS DiffCompressedSizeMBMax ' + @crlf - + N' , bL.MBpsAvg AS LogMBpsAvg ' + @crlf - + N' , bL.MBpsMin AS LogMBpsMin ' + @crlf - + N' , bL.MBpsMax AS LogMBpsMax ' + @crlf - + N' , bL.SizeMBAvg AS LogSizeMBAvg ' + @crlf - + N' , bL.SizeMBMin AS LogSizeMBMin ' + @crlf - + N' , bL.SizeMBMax AS LogSizeMBMax ' + @crlf - + N' , bL.CompressedSizeMBAvg AS LogCompressedSizeMBAvg ' + @crlf - + N' , bL.CompressedSizeMBMin AS LogCompressedSizeMBMin ' + @crlf - + N' , bL.CompressedSizeMBMax AS LogCompressedSizeMBMax ' + @crlf - + N' FROM Backups bF ' + @crlf - + N' LEFT OUTER JOIN Backups bD ON bF.database_name = bD.database_name AND bF.database_guid = bD.database_guid AND bD.backup_type = ''I''' + @crlf - + N' LEFT OUTER JOIN Backups bL ON bF.database_name = bL.database_name AND bF.database_guid = bL.database_guid AND bL.backup_type = ''L''' + @crlf - + N' WHERE bF.backup_type = ''D''; ' + @crlf; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'SELECT + 8 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Damaged backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' damaged backups taken without stopping to throw an error. This is done by specifying CONTINUE_AFTER_ERROR in your BACKUP commands.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.is_damaged = 1 + GROUP BY b.database_name;' + @crlf; IF @Debug = 1 PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*Checking for encrypted backups and the last backup of the encryption key.*/ + /*2014 ONLY*/ - RAISERROR('Updating #Backups with worst RPO case', 0, 1) WITH NOWAIT; +IF @ProductVersionMajor >= 12 + BEGIN + + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'SELECT + 9 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Encrypted backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' '' + b.encryptor_type + '' backups, and the last time a certificate was backed up is ' + + CASE WHEN LOWER(@MSDBName) <> N'msdb' + THEN + N'...well, that information is on another server, anyway.'' AS [Warning]' + ELSE + CONVERT(VARCHAR(30), (SELECT MAX(c.pvt_key_last_backup_date) FROM sys.certificates AS c WHERE c.name NOT LIKE '##%')) + N'.'' AS [Warning]' + END + + N' + FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b + WHERE b.encryptor_type IS NOT NULL + GROUP BY b.database_name, b.encryptor_type;' + @crlf; + + IF @Debug = 1 + PRINT @StringToExecute; + + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + END + + /*Looking for backups that have BULK LOGGED data in them -- this can screw up point in time LOG recovery.*/ SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N' - SELECT bs.database_name, bs.database_guid, bs.backup_set_id, bsPrior.backup_set_id AS backup_set_id_prior, - bs.backup_finish_date, bsPrior.backup_finish_date AS backup_finish_date_prior, - DATEDIFF(ss, bsPrior.backup_finish_date, bs.backup_finish_date) AS backup_gap_seconds - INTO #backup_gaps - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs - CROSS APPLY ( - SELECT TOP 1 bs1.backup_set_id, bs1.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs1 - WHERE bs.database_name = bs1.database_name - AND bs.database_guid = bs1.database_guid - AND bs.backup_finish_date > bs1.backup_finish_date - AND bs.backup_set_id > bs1.backup_set_id - ORDER BY bs1.backup_finish_date DESC, bs1.backup_set_id DESC - ) bsPrior - WHERE bs.backup_finish_date > @StartTime - - CREATE CLUSTERED INDEX cx_backup_gaps ON #backup_gaps (database_name, database_guid, backup_set_id, backup_finish_date, backup_gap_seconds); - - WITH max_gaps AS ( - SELECT g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, - g.backup_finish_date, MAX(g.backup_gap_seconds) AS max_backup_gap_seconds - FROM #backup_gaps AS g - GROUP BY g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, g.backup_finish_date - ) - UPDATE #Backups - SET RPOWorstCaseMinutes = bg.max_backup_gap_seconds / 60.0 - , RPOWorstCaseBackupSetID = bg.backup_set_id - , RPOWorstCaseBackupSetFinishTime = bg.backup_finish_date - , RPOWorstCaseBackupSetIDPrior = bg.backup_set_id_prior - , RPOWorstCaseBackupSetPriorFinishTime = bg.backup_finish_date_prior - FROM #Backups b - INNER HASH JOIN max_gaps bg ON b.database_name = bg.database_name AND b.database_guid = bg.database_guid - LEFT OUTER HASH JOIN max_gaps bgBigger ON bg.database_name = bgBigger.database_name AND bg.database_guid = bgBigger.database_guid AND bg.max_backup_gap_seconds < bgBigger.max_backup_gap_seconds - WHERE bgBigger.backup_set_id IS NULL; - '; + SET @StringToExecute += N'SELECT + 10 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Bulk logged backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' backups with bulk logged data. This can make point in time recovery awkward. '' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b + WHERE b.has_bulk_logged_data = 1 + GROUP BY b.database_name;' + @crlf; + IF @Debug = 1 PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - RAISERROR('Updating #Backups with worst RPO case queries', 0, 1) WITH NOWAIT; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + + /*Looking for recovery model being switched between FULL and SIMPLE, because it''s a bad practice.*/ - UPDATE #Backups - SET RPOWorstCaseMoreInfoQuery = @MoreInfoHeader + N'SELECT * ' + @crlf - + N' FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset ' + @crlf - + N' WHERE database_name = ''' + database_name + ''' ' + @crlf - + N' AND database_guid = ''' + CAST(database_guid AS NVARCHAR(50)) + ''' ' + @crlf - + N' AND backup_finish_date >= DATEADD(hh, -2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' AND backup_finish_date <= DATEADD(hh, 2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' ORDER BY backup_finish_date;' - + @MoreInfoFooter; + SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute += N'SELECT + 11 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Recovery model switched'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has changed recovery models from between FULL and SIMPLE '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.recovery_model)) + '' times. This breaks the log chain and is generally a bad idea.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b + WHERE b.recovery_model <> ''BULK-LOGGED'' + GROUP BY b.database_name + HAVING COUNT(DISTINCT b.recovery_model) > 4;' + @crlf; -/* RTO */ + IF @Debug = 1 + PRINT @StringToExecute; -RAISERROR('Gathering RTO information', 0, 1) WITH NOWAIT; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; + /*Looking for uncompressed backups.*/ SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, log_last_lsn) - SELECT database_name, database_guid, MAX(last_lsn) AS log_last_lsn - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset bLastLog - WHERE type = ''L'' - AND bLastLog.backup_finish_date >= @StartTime - GROUP BY database_name, database_guid; - '; + SET @StringToExecute += N'SELECT + 12 AS CheckId, + 100 AS [Priority], + b.database_name AS [Database Name], + ''Uncompressed backups'' AS [Finding], + ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' uncompressed backups in the last 30 days. This is a free way to save time and space. And SPACETIME. If your version of SQL supports it.'' AS [Warning] + FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b + WHERE backup_size = compressed_backup_size AND type = ''D'' + AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) + GROUP BY b.database_name;' + @crlf; IF @Debug = 1 PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + EXEC sys.sp_executesql @StringToExecute; -/* Find the most recent full backups for those logs */ +RAISERROR('Rules analysis starting on temp tables', 0, 1) WITH NOWAIT; -RAISERROR('Updating #RTORecoveryPoints', 0, 1) WITH NOWAIT; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + SELECT + 13 AS CheckId, + 100 AS Priority, + r.DatabaseName as [DatabaseName], + 'Big Diffs' AS [Finding], + 'On average, Differential backups for this database are >=40% of the size of the average Full backup.' AS [Warning] + FROM #Recoverability AS r + WHERE r.IsBigDiff = 1 - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + SELECT + 13 AS CheckId, + 100 AS Priority, + r.DatabaseName as [DatabaseName], + 'Big Logs' AS [Finding], + 'On average, Log backups for this database are >=20% of the size of the average Full backup.' AS [Warning] + FROM #Recoverability AS r + WHERE r.IsBigLog = 1 - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET log_backup_set_id = bLasted.backup_set_id - ,full_backup_set_id = bLasted.backup_set_id - ,full_last_lsn = bLasted.last_lsn - ,full_backup_set_uuid = bLasted.backup_set_uuid - FROM #RTORecoveryPoints rp - CROSS APPLY ( - SELECT TOP 1 bLog.backup_set_id AS backup_set_id_log, bLastFull.backup_set_id, bLastFull.last_lsn, bLastFull.backup_set_uuid, bLastFull.database_guid, bLastFull.database_name - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLastFull - ON bLog.database_guid = bLastFull.database_guid - AND bLog.database_name = bLastFull.database_name - AND bLog.first_lsn > bLastFull.last_lsn - AND bLastFull.type = ''D'' - WHERE rp.database_guid = bLog.database_guid - AND rp.database_name = bLog.database_name - ) bLasted - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLaterFulls ON bLasted.database_guid = bLaterFulls.database_guid AND bLasted.database_name = bLaterFulls.database_name - AND bLasted.last_lsn < bLaterFulls.last_lsn - AND bLaterFulls.first_lsn < bLasted.last_lsn - AND bLaterFulls.type = ''D'' - WHERE bLaterFulls.backup_set_id IS NULL; - '; - IF @Debug = 1 - PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute; +/*Insert thank you stuff last*/ + INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) -/* Add any full backups in the StartDate range that weren't part of the above log backup chain */ + SELECT + 2147483647 AS [CheckId], + 2147483647 AS [Priority], + 'From Your Community Volunteers' AS [DatabaseName], + 'sp_BlitzBackups Version: ' + @Version + ', Version Date: ' + CONVERT(VARCHAR(30), @VersionDate) + '.' AS [Finding], + 'Thanks for using our stored procedure. We hope you find it useful! Check out our other free SQL Server scripts at firstresponderkit.org!' AS [Warning]; -RAISERROR('Add any full backups in the StartDate range that weren''t part of the above log backup chain', 0, 1) WITH NOWAIT; +RAISERROR('Rules analysis finished', 0, 1) WITH NOWAIT; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; +SELECT w.CheckId, w.Priority, w.DatabaseName, w.Finding, w.Warning +FROM #Warnings AS w +ORDER BY w.Priority, w.CheckId; - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, full_backup_set_id, full_last_lsn, full_backup_set_uuid) - SELECT bFull.database_name, bFull.database_guid, bFull.backup_set_id, bFull.last_lsn, bFull.backup_set_uuid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull - LEFT OUTER JOIN #RTORecoveryPoints rp ON bFull.backup_set_uuid = rp.full_backup_set_uuid - WHERE bFull.type = ''D'' - AND bFull.backup_finish_date IS NOT NULL - AND rp.full_backup_set_uuid IS NULL - AND bFull.backup_finish_date >= @StartTime; - '; +DROP TABLE #Backups, #Warnings, #Recoverability, #RTORecoveryPoints - IF @Debug = 1 - PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; +RETURN; -/* Fill out the most recent log for that full, but before the next full */ +PushBackupHistoryToListener: -RAISERROR('Fill out the most recent log for that full, but before the next full', 0, 1) WITH NOWAIT; +RAISERROR('Pushing backup history to listener', 0, 1) WITH NOWAIT; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; +DECLARE @msg NVARCHAR(4000) = N''; +DECLARE @RemoteCheck TABLE (c INT NULL); - SET @StringToExecute += N' - UPDATE rp - SET log_last_lsn = (SELECT MAX(last_lsn) FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog WHERE bLog.first_lsn >= rp.full_last_lsn AND bLog.first_lsn <= rpNextFull.full_last_lsn AND bLog.type = ''L'') - FROM #RTORecoveryPoints rp - INNER JOIN #RTORecoveryPoints rpNextFull ON rp.database_guid = rpNextFull.database_guid AND rp.database_name = rpNextFull.database_name - AND rp.full_last_lsn < rpNextFull.full_last_lsn - LEFT OUTER JOIN #RTORecoveryPoints rpEarlierFull ON rp.database_guid = rpEarlierFull.database_guid AND rp.database_name = rpEarlierFull.database_name - AND rp.full_last_lsn < rpEarlierFull.full_last_lsn - AND rpNextFull.full_last_lsn > rpEarlierFull.full_last_lsn - WHERE rpEarlierFull.full_backup_set_id IS NULL; - '; - IF @Debug = 1 - PRINT @StringToExecute; +IF @WriteBackupsToDatabaseName IS NULL + BEGIN + RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 16, 1) WITH NOWAIT + RETURN; + END - EXEC sys.sp_executesql @StringToExecute; +IF LOWER(@WriteBackupsToDatabaseName) = N'msdb' + BEGIN + RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 16, 1) WITH NOWAIT + RETURN; + END -/* Fill out a diff in that range */ +IF @WriteBackupsToListenerName IS NULL +BEGIN + IF @AGName IS NULL + BEGIN + RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 16, 1) WITH NOWAIT; + RETURN; + END + ELSE + BEGIN + SELECT @WriteBackupsToListenerName = dns_name + FROM sys.availability_groups AS ag + JOIN sys.availability_group_listeners AS agl + ON ag.group_id = agl.group_id + WHERE name = @AGName; + END -RAISERROR('Fill out a diff in that range', 0, 1) WITH NOWAIT; +END + +IF @WriteBackupsToListenerName IS NOT NULL +BEGIN + IF NOT EXISTS + ( + SELECT * + FROM sys.servers s + WHERE name = @WriteBackupsToListenerName + ) + BEGIN + SET @msg = N'We need a linked server to write data across. Please set one up for ' + @WriteBackupsToListenerName + N'.'; + RAISERROR(@msg, 16, 1) WITH NOWAIT; + RETURN; + END +END SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET diff_last_lsn = (SELECT TOP 1 bDiff.last_lsn FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff - WHERE rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name - AND bDiff.type = ''I'' - AND bDiff.last_lsn < rp.log_last_lsn - AND rp.full_backup_set_uuid = bDiff.differential_base_guid - ORDER BY bDiff.last_lsn DESC) - FROM #RTORecoveryPoints rp - WHERE diff_last_lsn IS NULL; - '; + SET @StringToExecute += N'SELECT TOP 1 1 FROM ' + + QUOTENAME(@WriteBackupsToListenerName) + N'.master.sys.databases d WHERE d.name = @i_WriteBackupsToDatabaseName;' IF @Debug = 1 PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute; - -/* Get time & size totals for full & diff */ - -RAISERROR('Get time & size totals for full & diff', 0, 1) WITH NOWAIT; + INSERT @RemoteCheck (c) + EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsToDatabaseName NVARCHAR(256)', @i_WriteBackupsToDatabaseName = @WriteBackupsToDatabaseName; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + IF @@ROWCOUNT = 0 + BEGIN + SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to exist on that server.' + RAISERROR(@msg, 16, 1) WITH NOWAIT + RETURN; + END - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET full_time_seconds = DATEDIFF(ss,bFull.backup_start_date, bFull.backup_finish_date) - , full_file_size_mb = bFull.backup_size / 1048576.0 - , diff_backup_set_id = bDiff.backup_set_id - , diff_time_seconds = DATEDIFF(ss,bDiff.backup_start_date, bDiff.backup_finish_date) - , diff_file_size_mb = bDiff.backup_size / 1048576.0 - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull ON rp.database_guid = bFull.database_guid AND rp.database_name = bFull.database_name AND rp.full_last_lsn = bFull.last_lsn - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff ON rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name AND rp.diff_last_lsn = bDiff.last_lsn AND bDiff.last_lsn IS NOT NULL; - '; + + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'SELECT TOP 1 1 FROM ' + + QUOTENAME(@WriteBackupsToListenerName) + '.' + QUOTENAME(@WriteBackupsToDatabaseName) + '.sys.tables WHERE name = ''backupset'' AND SCHEMA_NAME(schema_id) = ''dbo''; + ' + @crlf; IF @Debug = 1 PRINT @StringToExecute; + + INSERT @RemoteCheck (c) + EXEC sp_executesql @StringToExecute; + + IF @@ROWCOUNT = 0 + BEGIN + + SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to have a table called dbo.backupset in it.' + RAISERROR(@msg, 0, 1) WITH NOWAIT + RAISERROR('Don''t worry, we''ll create it for you!', 0, 1) WITH NOWAIT + + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'CREATE TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset + ( backup_set_id INT IDENTITY(1, 1), backup_set_uuid UNIQUEIDENTIFIER, media_set_id INT, first_family_number TINYINT, first_media_number SMALLINT, + last_family_number TINYINT, last_media_number SMALLINT, catalog_family_number TINYINT, catalog_media_number SMALLINT, position INT, expiration_date DATETIME, + software_vendor_id INT, name NVARCHAR(128), description NVARCHAR(255), user_name NVARCHAR(128), software_major_version TINYINT, software_minor_version TINYINT, + software_build_version SMALLINT, time_zone SMALLINT, mtf_minor_version TINYINT, first_lsn NUMERIC(25, 0), last_lsn NUMERIC(25, 0), checkpoint_lsn NUMERIC(25, 0), + database_backup_lsn NUMERIC(25, 0), database_creation_date DATETIME, backup_start_date DATETIME, backup_finish_date DATETIME, type CHAR(1), sort_order SMALLINT, + code_page SMALLINT, compatibility_level TINYINT, database_version INT, backup_size NUMERIC(20, 0), database_name NVARCHAR(128), server_name NVARCHAR(128), + machine_name NVARCHAR(128), flags INT, unicode_locale INT, unicode_compare_style INT, collation_name NVARCHAR(128), is_password_protected BIT, recovery_model NVARCHAR(60), + has_bulk_logged_data BIT, is_snapshot BIT, is_readonly BIT, is_single_user BIT, has_backup_checksums BIT, is_damaged BIT, begins_log_chain BIT, has_incomplete_metadata BIT, + is_force_offline BIT, is_copy_only BIT, first_recovery_fork_guid UNIQUEIDENTIFIER, last_recovery_fork_guid UNIQUEIDENTIFIER, fork_point_lsn NUMERIC(25, 0), database_guid UNIQUEIDENTIFIER, + family_guid UNIQUEIDENTIFIER, differential_base_lsn NUMERIC(25, 0), differential_base_guid UNIQUEIDENTIFIER, compressed_backup_size NUMERIC(20, 0), key_algorithm NVARCHAR(32), + encryptor_thumbprint VARBINARY(20) , encryptor_type NVARCHAR(32) + ); + ' + @crlf; + + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - EXEC sys.sp_executesql @StringToExecute; + IF @Debug = 1 + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute -/* Get time & size totals for logs */ + RAISERROR('We''ll even make the indexes!', 0, 1) WITH NOWAIT -RAISERROR('Get time & size totals for logs', 0, 1) WITH NOWAIT; + /*Checking for and creating the PK/CX*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N' + + IF NOT EXISTS ( + SELECT t.name, i.name + FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i + ON t.object_id = i.object_id + WHERE t.name = ? + AND i.name LIKE ? + ) - SET @StringToExecute += N' - WITH LogTotals AS ( - SELECT rp.id, log_time_seconds = SUM(DATEDIFF(ss,bLog.backup_start_date, bLog.backup_finish_date)) - , log_file_size = SUM(bLog.backup_size) - , SUM(1) AS log_backups - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog ON rp.database_guid = bLog.database_guid AND rp.database_name = bLog.database_name AND bLog.type = ''L'' - AND bLog.first_lsn > COALESCE(rp.diff_last_lsn, rp.full_last_lsn) - AND bLog.first_lsn <= rp.log_last_lsn - GROUP BY rp.id - ) - UPDATE #RTORecoveryPoints - SET log_time_seconds = lt.log_time_seconds - , log_file_size_mb = lt.log_file_size / 1048576.0 - , log_backups = lt.log_backups - FROM #RTORecoveryPoints rp - INNER JOIN LogTotals lt ON rp.id = lt.id; - '; + BEGIN + ALTER TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ADD PRIMARY KEY CLUSTERED ([backup_set_id] ASC) + END + ' + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''PK[_][_]%'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + IF @Debug = 1 - PRINT @StringToExecute; + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute - EXEC sys.sp_executesql @StringToExecute; -RAISERROR('Gathering RTO worst cases', 0, 1) WITH NOWAIT; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + /*Checking for and creating index on backup_set_uuid*/ - SET @StringToExecute += N' - WITH WorstCases AS ( - SELECT rp.* - FROM #RTORecoveryPoints rp - LEFT OUTER JOIN #RTORecoveryPoints rpNewer - ON rp.database_guid = rpNewer.database_guid - AND rp.database_name = rpNewer.database_name - AND rp.full_last_lsn < rpNewer.full_last_lsn - AND rpNewer.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - WHERE rp.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - /* OR rp.rto_worst_case_time_seconds = (SELECT TOP 1 rto_worst_case_time_seconds FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_time_seconds DESC) */ - AND rpNewer.database_guid IS NULL - ) - UPDATE #Backups - SET RTOWorstCaseMinutes = - /* Fulls */ - (CASE WHEN @RestoreSpeedFullMBps IS NULL - THEN wc.full_time_seconds / 60.0 - ELSE @RestoreSpeedFullMBps / wc.full_file_size_mb - END) + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'IF NOT EXISTS ( + SELECT t.name, i.name + FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i + ON t.object_id = i.object_id + WHERE t.name = ? + AND i.name = ? + ) - /* Diffs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedDiffMBps IS NOT NULL AND wc.diff_file_size_mb IS NOT NULL - THEN @RestoreSpeedDiffMBps / wc.diff_file_size_mb - ELSE COALESCE(wc.diff_time_seconds,0) / 60.0 - END) + BEGIN + CREATE NONCLUSTERED INDEX [backupsetuuid] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_set_uuid] ASC) + END + ' - /* Logs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedLogMBps IS NOT NULL AND wc.log_file_size_mb IS NOT NULL - THEN @RestoreSpeedLogMBps / wc.log_file_size_mb - ELSE COALESCE(wc.log_time_seconds,0) / 60.0 - END) - , RTOWorstCaseBackupFileSizeMB = wc.rto_worst_case_size_mb - FROM #Backups b - INNER JOIN WorstCases wc - ON b.database_guid = wc.database_guid - AND b.database_name = wc.database_name; - '; + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetuuid'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + + IF @Debug = 1 + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute + + + + /*Checking for and creating index on media_set_id*/ + + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += 'IF NOT EXISTS ( + SELECT t.name, i.name + FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i + ON t.object_id = i.object_id + WHERE t.name = ? + AND i.name = ? + ) + BEGIN + CREATE NONCLUSTERED INDEX [backupsetMediaSetId] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([media_set_id] ASC) + END + ' + + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetMediaSetId'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + IF @Debug = 1 - PRINT @StringToExecute; + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute + + + + /*Checking for and creating index on backup_finish_date*/ - EXEC sys.sp_executesql @StringToExecute, N'@RestoreSpeedFullMBps INT, @RestoreSpeedDiffMBps INT, @RestoreSpeedLogMBps INT', @RestoreSpeedFullMBps, @RestoreSpeedDiffMBps, @RestoreSpeedLogMBps; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'IF NOT EXISTS ( + SELECT t.name, i.name + FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i + ON t.object_id = i.object_id + WHERE t.name = ? + AND i.name = ? + ) + BEGIN + CREATE NONCLUSTERED INDEX [backupsetDate] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_finish_date] ASC) + END + ' + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDate'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + + IF @Debug = 1 + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute -/*Populating Recoverability*/ + + /*Checking for and creating index on database_name*/ - /*Get distinct list of databases*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + + SET @StringToExecute += N'IF NOT EXISTS ( + SELECT t.name, i.name + FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t + JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i + ON t.object_id = i.object_id + WHERE t.name = ? + AND i.name = ? + ) - SET @StringToExecute += N' - SELECT DISTINCT b.database_name, database_guid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b;' + BEGIN + CREATE NONCLUSTERED INDEX [backupsetDatabaseName] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([database_name] ASC ) INCLUDE ([backup_set_id], [media_set_id]) + END + + ' + SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDatabaseName'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + IF @Debug = 1 - PRINT @StringToExecute; + PRINT @InnerStringToExecute; + + EXEC sp_executesql @InnerStringToExecute - INSERT #Recoverability ( DatabaseName, DatabaseGUID ) - EXEC sys.sp_executesql @StringToExecute; + RAISERROR('Table and indexes created! You''re welcome!', 0, 1) WITH NOWAIT + END - /*Find most recent recovery model, backup size, and backup date*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + RAISERROR('Beginning inserts', 0, 1) WITH NOWAIT; + RAISERROR(@crlf, 0, 1) WITH NOWAIT; - SET @StringToExecute += N' - UPDATE r - SET r.LastBackupRecoveryModel = ca.recovery_model, - r.LastFullBackupSizeMB = ca.compressed_backup_size, - r.LastFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 b.recovery_model, (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date DESC - ) ca;' - - IF @Debug = 1 - PRINT @StringToExecute; + /* + Batching code comes from the lovely and talented Michael J. Swart + http://michaeljswart.com/2014/09/take-care-when-scripting-batches/ + If you're ever in Canada, he says you can stay at his house, too. + */ - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - /*Find first backup size and date*/ - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N' - UPDATE r - SET r.FirstFullBackupSizeMB = ca.compressed_backup_size, - r.FirstFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date ASC - ) ca;' + SET @StringToExecute += N' + DECLARE + @StartDate DATETIME = DATEADD(HOUR, @i_WriteBackupsLastHours, SYSDATETIME()), + @StartDateNext DATETIME, + @RC INT = 1, + @msg NVARCHAR(4000) = N''''; + + SELECT @StartDate = MIN(b.backup_start_date) + FROM msdb.dbo.backupset b + WHERE b.backup_start_date >= @StartDate + AND NOT EXISTS ( + SELECT 1 + FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 + WHERE b.backup_set_uuid = b2.backup_set_uuid + AND b2.backup_start_date >= @StartDate + ) - IF @Debug = 1 - PRINT @StringToExecute; + SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + IF + ( @StartDate IS NULL ) + BEGIN + SET @msg = N''No data to move, exiting.'' + RAISERROR(@msg, 0, 1) WITH NOWAIT + RETURN; + END - /*Find average backup throughputs for full, diff, and log*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + RAISERROR(''Starting insert loop'', 0, 1) WITH NOWAIT; - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullBackupThroughputMB = ca_full.AvgFullSpeed, - r.AvgDiffBackupThroughputMB = ca_diff.AvgDiffSpeed, - r.AvgLogBackupThroughputMB = ca_log.AvgLogSpeed, - r.AvgFullBackupDurationSeconds = AvgFullDuration, - r.AvgDiffBackupDurationSeconds = AvgDiffDuration, - r.AvgLogBackupDurationSeconds = AvgLogDuration - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgFullSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgFullDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_full - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgDiffSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgDiffDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_diff - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgLogSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgLogDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_log;' + WHILE EXISTS ( + SELECT 1 + FROM msdb.dbo.backupset b + WHERE NOT EXISTS ( + SELECT 1 + FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 + WHERE b.backup_set_uuid = b2.backup_set_uuid + AND b2.backup_start_date >= @StartDate + ) + ) + BEGIN + + SET @msg = N''Inserting data for '' + CONVERT(NVARCHAR(30), @StartDate) + '' through '' + + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' + RAISERROR(@msg, 0, 1) WITH NOWAIT + + ' - IF @Debug = 1 - PRINT @StringToExecute; + SET @StringToExecute += N'INSERT ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset + ' + SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, time_zone, + compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, + is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 + THEN + N'encryptor_type, has_bulk_logged_data)' + @crlf + ELSE + N'has_bulk_logged_data)' + @crlf + END + + SET @StringToExecute +=N' + SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, time_zone, + compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, + is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 + THEN + N'encryptor_type, has_bulk_logged_data' + @crlf + ELSE + N'has_bulk_logged_data' + @crlf + END + SET @StringToExecute +=N' + FROM msdb.dbo.backupset b + WHERE 1=1 + AND b.backup_start_date >= @StartDate + AND b.backup_start_date < @StartDateNext + AND NOT EXISTS ( + SELECT 1 + FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 + WHERE b.backup_set_uuid = b2.backup_set_uuid + AND b2.backup_start_date >= @StartDate + )' + @crlf; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; + SET @StringToExecute +=N' + SET @RC = @@ROWCOUNT; + + SET @msg = N''Inserted '' + CONVERT(NVARCHAR(30), @RC) + '' rows for ''+ CONVERT(NVARCHAR(30), @StartDate) + '' through '' + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' + RAISERROR(@msg, 0, 1) WITH NOWAIT + + SET @StartDate = @StartDateNext; + SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - /*Find max and avg diff and log sizes*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + IF + ( @StartDate > SYSDATETIME() ) + BEGIN + + SET @msg = N''No more data to move, exiting.'' + RAISERROR(@msg, 0, 1) WITH NOWAIT + + BREAK; - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullSizeMB = fulls.avg_full_size, - r.AvgDiffSizeMB = diffs.avg_diff_size, - r.AvgLogSizeMB = logs.avg_log_size - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_full_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS fulls - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_diff_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS diffs - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_log_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS logs;' + END + END' + @crlf; IF @Debug = 1 PRINT @StringToExecute; - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/*Trending - only works if backupfile is populated, which means in msdb */ -IF @MSDBName = N'msdb' -BEGIN - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' --+ @crlf; - - SET @StringToExecute += N' - SELECT p.DatabaseName, - p.DatabaseGUID, - p.[0], - p.[-1], - p.[-2], - p.[-3], - p.[-4], - p.[-5], - p.[-6], - p.[-7], - p.[-8], - p.[-9], - p.[-10], - p.[-11], - p.[-12] - FROM ( SELECT b.database_name AS DatabaseName, - b.database_guid AS DatabaseGUID, - DATEDIFF(MONTH, @StartTime, b.backup_start_date) AS MonthsAgo , - CONVERT(DECIMAL(18, 2), AVG(bf.file_size / 1048576.0)) AS AvgSizeMB - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupfile AS bf - ON b.backup_set_id = bf.backup_set_id - WHERE b.database_name NOT IN ( ''master'', ''msdb'', ''model'', ''tempdb'' ) - AND bf.file_type = ''D'' - AND b.backup_start_date >= DATEADD(YEAR, -1, @StartTime) - AND b.backup_start_date <= SYSDATETIME() - GROUP BY b.database_name, - b.database_guid, - DATEDIFF(mm, @StartTime, b.backup_start_date) - ) AS bckstat PIVOT ( SUM(bckstat.AvgSizeMB) FOR bckstat.MonthsAgo IN ( [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) ) AS p - ORDER BY p.DatabaseName; - ' - - IF @Debug = 1 - PRINT @StringToExecute; + EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsLastHours INT', @i_WriteBackupsLastHours = @WriteBackupsLastHours; - INSERT #Trending ( DatabaseName, DatabaseGUID, [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; +END; -END +END; -/*End Trending*/ +GO +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 STATISTICS IO OFF; +SET STATISTICS TIME OFF; +GO -/*End populating Recoverability*/ +IF ( +SELECT + CASE + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 + ELSE 1 + END +) = 0 +BEGIN + DECLARE @msg VARCHAR(8000); + SELECT @msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); + PRINT @msg; + RETURN; +END; -RAISERROR('Returning data', 0, 1) WITH NOWAIT; +IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); +GO - SELECT b.* - FROM #Backups AS b - ORDER BY b.database_name; +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheProcs', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheProcs;'); +GO - SELECT r.*, - t.[0], t.[-1], t.[-2], t.[-3], t.[-4], t.[-5], t.[-6], t.[-7], t.[-8], t.[-9], t.[-10], t.[-11], t.[-12] - FROM #Recoverability AS r - LEFT JOIN #Trending t - ON r.DatabaseName = t.DatabaseName - AND r.DatabaseGUID = t.DatabaseGUID - WHERE r.LastBackupRecoveryModel IS NOT NULL - ORDER BY r.DatabaseName +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheResults', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheResults;'); +GO +CREATE TABLE ##BlitzCacheResults ( + SPID INT, + ID INT IDENTITY(1,1), + CheckID INT, + Priority TINYINT, + FindingsGroup VARCHAR(50), + Finding VARCHAR(500), + URL VARCHAR(200), + Details VARCHAR(4000) +); -RAISERROR('Rules analysis starting', 0, 1) WITH NOWAIT; +CREATE TABLE ##BlitzCacheProcs ( + SPID INT , + QueryType NVARCHAR(258), + DatabaseName sysname, + AverageCPU DECIMAL(38,4), + AverageCPUPerMinute DECIMAL(38,4), + TotalCPU DECIMAL(38,4), + PercentCPUByType MONEY, + PercentCPU MONEY, + AverageDuration DECIMAL(38,4), + TotalDuration DECIMAL(38,4), + PercentDuration MONEY, + PercentDurationByType MONEY, + AverageReads BIGINT, + TotalReads BIGINT, + PercentReads MONEY, + PercentReadsByType MONEY, + ExecutionCount BIGINT, + PercentExecutions MONEY, + PercentExecutionsByType MONEY, + ExecutionsPerMinute MONEY, + TotalWrites BIGINT, + AverageWrites MONEY, + PercentWrites MONEY, + PercentWritesByType MONEY, + WritesPerMinute MONEY, + PlanCreationTime DATETIME, + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' + ELSE 'N/A' END, + SqlHandle VARBINARY(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' + ELSE 'N/A' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' + ELSE 'N/A' END, + QueryHash BINARY(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' + ELSE 'N/A' END, + QueryPlanHash BINARY(8), + StatementStartOffset INT, + StatementEndOffset INT, + PlanGenerationNum BIGINT, + MinReturnedRows BIGINT, + MaxReturnedRows BIGINT, + AverageReturnedRows MONEY, + TotalReturnedRows BIGINT, + LastReturnedRows BIGINT, + /*The Memory Grant columns are only supported + in certain versions, giggle giggle. + */ + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryText NVARCHAR(MAX), + QueryPlan XML, + /* these next four columns are the total for the type of query. + don't actually use them for anything apart from math by type. + */ + TotalWorkerTimeForType BIGINT, + TotalElapsedTimeForType BIGINT, + TotalReadsForType DECIMAL(30), + TotalExecutionCountForType BIGINT, + TotalWritesForType DECIMAL(30), + NumberOfPlans INT, + NumberOfDistinctPlans INT, + SerialDesiredMemory FLOAT, + SerialRequiredMemory FLOAT, + CachedPlanSize FLOAT, + CompileTime FLOAT, + CompileCPU FLOAT , + CompileMemory FLOAT , + MaxCompileMemory FLOAT , + min_worker_time BIGINT, + max_worker_time BIGINT, + is_forced_plan BIT, + is_forced_parameterized BIT, + is_cursor BIT, + is_optimistic_cursor BIT, + is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, + is_parallel BIT, + is_forced_serial BIT, + is_key_lookup_expensive BIT, + key_lookup_cost FLOAT, + is_remote_query_expensive BIT, + remote_query_cost FLOAT, + frequent_execution BIT, + parameter_sniffing BIT, + unparameterized_query BIT, + near_parallel BIT, + plan_warnings BIT, + plan_multiple_plans INT, + long_running BIT, + downlevel_estimator BIT, + implicit_conversions BIT, + busy_loops BIT, + tvf_join BIT, + tvf_estimate BIT, + compile_timeout BIT, + compile_memory_limit_exceeded BIT, + warning_no_join_predicate BIT, + QueryPlanCost FLOAT, + missing_index_count INT, + unmatched_index_count INT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + age_minutes MONEY, + age_minutes_lifetime MONEY, + is_trivial BIT, + trace_flags_session VARCHAR(1000), + is_unused_grant BIT, + function_count INT, + clr_function_count INT, + is_table_variable BIT, + no_stats_warning BIT, + relop_warnings BIT, + is_table_scan BIT, + backwards_scan BIT, + forced_index BIT, + forced_seek BIT, + forced_scan BIT, + columnstore_row_mode BIT, + is_computed_scalar BIT , + is_sort_expensive BIT, + sort_cost FLOAT, + is_computed_filter BIT, + op_name VARCHAR(100) NULL, + index_insert_count INT NULL, + index_update_count INT NULL, + index_delete_count INT NULL, + cx_insert_count INT NULL, + cx_update_count INT NULL, + cx_delete_count INT NULL, + table_insert_count INT NULL, + table_update_count INT NULL, + table_delete_count INT NULL, + index_ops AS (index_insert_count + index_update_count + index_delete_count + + cx_insert_count + cx_update_count + cx_delete_count + + table_insert_count + table_update_count + table_delete_count), + is_row_level BIT, + is_spatial BIT, + index_dml BIT, + table_dml BIT, + long_running_low_cpu BIT, + low_cost_high_cpu BIT, + stale_stats BIT, + is_adaptive BIT, + index_spool_cost FLOAT, + index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, + is_spool_expensive BIT, + is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, + estimated_rows FLOAT, + is_bad_estimate BIT, + is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, + implicit_conversion_info XML, + cached_execution_parameters XML, + missing_indexes XML, + SetOptions VARCHAR(MAX), + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) + ); +GO -/*Looking for out of band backups by finding most common backup operator user_name and noting backups taken by other user_names*/ +ALTER PROCEDURE dbo.sp_BlitzCache + @Help BIT = 0, + @Top INT = NULL, + @SortOrder VARCHAR(50) = 'CPU', + @UseTriggersAnyway BIT = NULL, + @ExportToExcel BIT = 0, + @ExpertMode TINYINT = 0, + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(258) = NULL , + @OutputDatabaseName NVARCHAR(258) = NULL , + @OutputSchemaName NVARCHAR(258) = NULL , + @OutputTableName NVARCHAR(258) = NULL , -- do NOT use ##BlitzCacheResults or ##BlitzCacheProcs as they are used as work tables in this procedure + @ConfigurationDatabaseName NVARCHAR(128) = NULL , + @ConfigurationSchemaName NVARCHAR(258) = NULL , + @ConfigurationTableName NVARCHAR(258) = NULL , + @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 , + @IgnoreSqlHandles VARCHAR(MAX) = NULL , + @QueryFilter VARCHAR(10) = 'ALL' , + @DatabaseName NVARCHAR(128) = NULL , + @StoredProcName NVARCHAR(128) = NULL, + @SlowlySearchPlansFor NVARCHAR(4000) = NULL, + @Reanalyze BIT = 0 , + @SkipAnalysis BIT = 0 , + @BringThePain BIT = 0 , + @MinimumExecutionCount INT = 0, + @Debug BIT = 0, + @CheckDateOverride DATETIMEOFFSET = NULL, + @MinutesBack INT = NULL, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @KeepCRLF BIT = 0 +WITH RECOMPILE +AS +BEGIN +SET NOCOUNT ON; +SET STATISTICS XML OFF; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); - SET @StringToExecute += N' - WITH common_people AS ( - SELECT TOP 1 b.user_name, COUNT_BIG(*) AS Records - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.user_name - ORDER BY Records DESC - ) - SELECT - 1 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Non-Agent backups taken'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up by '' + QUOTENAME(b.user_name) + '' '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.user_name NOT LIKE ''%Agent%'' AND b.user_name NOT LIKE ''%AGENT%'' - AND NOT EXISTS ( - SELECT 1 - FROM common_people AS cp - WHERE cp.user_name = b.user_name - ) - GROUP BY b.database_name, b.user_name - HAVING COUNT(*) > 1;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; - INSERT #Warnings (CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; +DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; - /*Looking for compatibility level changing. Only looking for databases that have changed more than twice (It''s possible someone may have changed up, had CE problems, and then changed back)*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; +IF @Help = 1 + BEGIN + PRINT ' + sp_BlitzCache from http://FirstResponderKit.org + + This script displays your most resource-intensive queries from the plan cache, + and points to ways you can tune these queries to make them faster. - SET @StringToExecute += N'SELECT - 2 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Compatibility level changing'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed compatibility levels '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.compatibility_level)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.compatibility_level) > 2;' + @crlf; - IF @Debug = 1 - PRINT @StringToExecute; + 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. - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for password protected backups. This hasn''t been a popular option ever, and was largely replaced by encrypted backups, but it''s simple to check for.*/ + Known limitations of this version: + - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is + excluded by default. + - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes + with no spaces between the hash values. - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + Unknown limitations of this version: + - May or may not be vulnerable to the wick effect. - SET @StringToExecute += N'SELECT - 3 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Password backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up with a password '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times. Who has the password?'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_password_protected = 1 - GROUP BY b.database_name;' + @crlf; + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - IF @Debug = 1 - PRINT @StringToExecute; - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for snapshot backups. There are legit reasons for these, but we should flag them so the questions get asked. What questions? Good question.*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + MIT License - SET @StringToExecute += N'SELECT - 4 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Snapshot backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' snapshot backups. This message is purely informational.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_snapshot = 1 - GROUP BY b.database_name;' + @crlf; + Copyright (c) Brent Ozar Unlimited - IF @Debug = 1 - PRINT @StringToExecute; + 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: - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*It''s fine to take backups of read only databases, but it''s not always necessary (there''s no new data, after all).*/ + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + 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. + '; - SET @StringToExecute += N'SELECT - 5 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Read only state backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in a read-only state. This can be normal if it''''s a secondary, but a bit odd otherwise.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_readonly = 1 - GROUP BY b.database_name;' + @crlf; + + SELECT N'@Help' AS [Parameter Name] , + N'BIT' AS [Data Type] , + N'Displays this help message.' AS [Parameter Description] - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'@Top', + N'INT', + N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*So, I''ve come across people who think they need to change their database to single user mode to take a backup. Or that doing that will help something. I just need to know, here.*/ + UNION ALL + SELECT N'@SortOrder', + N'VARCHAR(10)', + N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant", "Unused Grant", "Spills", "Query Hash", "Duplicate". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'@UseTriggersAnyway', + N'BIT', + N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' - SET @StringToExecute += N'SELECT - 6 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Single user mode backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in single-user mode. This is really weird! Make sure your backup process doesn''''t include a mode change anywhere.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_single_user = 1 - GROUP BY b.database_name;' + @crlf; + UNION ALL + SELECT N'@ExportToExcel', + N'BIT', + N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'@ExpertMode', + N'TINYINT', + N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' + UNION ALL + SELECT N'@OutputType', + N'NVARCHAR(258)', + N'If set to "NONE", this will tell this procedure not to run any query leading to a results set thrown to caller.' + + UNION ALL + SELECT N'@OutputDatabaseName', + N'NVARCHAR(128)', + N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*C''mon, it''s 2017. Take your backups with CHECKSUMS, people.*/ + UNION ALL + SELECT N'@OutputSchemaName', + N'NVARCHAR(258)', + N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'@OutputTableName', + N'NVARCHAR(258)', + N'The output table. If this does not exist, it will be created for you.' - SET @StringToExecute += N'SELECT - 7 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''No CHECKSUMS'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times without CHECKSUMS in the past 30 days. CHECKSUMS can help alert you to corruption errors.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.has_backup_checksums = 0 - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; + UNION ALL + SELECT N'@DurationFilter', + N'DECIMAL(38,4)', + N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'@HideSummary', + N'BIT', + N'Hides the findings summary result set.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Damaged is a Black Flag album. You don''t want your backups to be like a Black Flag album. */ + UNION ALL + SELECT N'@IgnoreSystemDBs', + N'BIT', + N'Ignores plans found in the system databases (master, model, msdb, tempdb, dbmaintenance, dbadmin, dbatools and resourcedb)' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'@OnlyQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' - SET @StringToExecute += N'SELECT - 8 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Damaged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' damaged backups taken without stopping to throw an error. This is done by specifying CONTINUE_AFTER_ERROR in your BACKUP commands.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_damaged = 1 - GROUP BY b.database_name;' + @crlf; + UNION ALL + SELECT N'@IgnoreQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to ignore.' + + UNION ALL + SELECT N'@OnlySqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to use for filtering results.' + + UNION ALL + SELECT N'@IgnoreSqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to ignore.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'@DatabaseName', + N'NVARCHAR(128)', + N'A database name which is used for filtering results.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Checking for encrypted backups and the last backup of the encryption key.*/ + UNION ALL + SELECT N'@StoredProcName', + N'NVARCHAR(128)', + N'Name of stored procedure you want to find plans for.' - /*2014 ONLY*/ + UNION ALL + SELECT N'@SlowlySearchPlansFor', + N'NVARCHAR(4000)', + N'String to search for in plan text. % wildcards allowed.' -IF @ProductVersionMajor >= 12 - BEGIN + UNION ALL + SELECT N'@BringThePain', + N'BIT', + N'When using @SortOrder = ''all'' and @Top > 10, we require you to set @BringThePain = 1 so you understand that sp_BlitzCache will take a while to run.' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'@QueryFilter', + N'VARCHAR(10)', + N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' - SET @StringToExecute += N'SELECT - 9 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Encrypted backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' '' + b.encryptor_type + '' backups, and the last time a certificate was backed up is ' - + CASE WHEN LOWER(@MSDBName) <> N'msdb' - THEN + N'...well, that information is on another server, anyway.'' AS [Warning]' - ELSE + CONVERT(VARCHAR(30), (SELECT MAX(c.pvt_key_last_backup_date) FROM sys.certificates AS c WHERE c.name NOT LIKE '##%')) + N'.'' AS [Warning]' - END + - N' - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.encryptor_type IS NOT NULL - GROUP BY b.database_name, b.encryptor_type;' + @crlf; + UNION ALL + SELECT N'@Reanalyze', + N'BIT', + N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' + + UNION ALL + SELECT N'@MinimumExecutionCount', + N'INT', + N'Queries with fewer than this number of executions will be omitted from results.' + + UNION ALL + SELECT N'@Debug', + N'BIT', + N'Setting this to 1 will print dynamic SQL and select data from all tables used.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'@MinutesBack', + N'INT', + N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negative.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - END + UNION ALL + SELECT N'@Version', + N'VARCHAR(30)', + N'OUTPUT parameter holding version number.' - /*Looking for backups that have BULK LOGGED data in them -- this can screw up point in time LOG recovery.*/ + UNION ALL + SELECT N'@VersionDate', + N'DATETIME', + N'OUTPUT parameter holding version date.' - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'@VersionCheckMode', + N'BIT', + N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.' - SET @StringToExecute += N'SELECT - 10 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Bulk logged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' backups with bulk logged data. This can make point in time recovery awkward. '' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.has_bulk_logged_data = 1 - GROUP BY b.database_name;' + @crlf; + UNION ALL + SELECT N'@KeepCRLF', + N'BIT', + N'Retain CR/LF in query text to avoid issues caused by line comments.'; - IF @Debug = 1 - PRINT @StringToExecute; - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for recovery model being switched between FULL and SIMPLE, because it''s a bad practice.*/ + /* Column definitions */ + SELECT N'# Executions' AS [Column Name], + N'BIGINT' AS [Data Type], + N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'Executions / Minute', + N'MONEY', + N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' - SET @StringToExecute += N'SELECT - 11 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Recovery model switched'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed recovery models from between FULL and SIMPLE '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.recovery_model)) + '' times. This breaks the log chain and is generally a bad idea.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.recovery_model <> ''BULK-LOGGED'' - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.recovery_model) > 4;' + @crlf; + UNION ALL + SELECT N'Execution Weight', + N'MONEY', + N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'Database', + N'sysname', + N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; + UNION ALL + SELECT N'Total CPU', + N'BIGINT', + N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' - /*Looking for uncompressed backups.*/ + UNION ALL + SELECT N'Avg CPU', + N'BIGINT', + N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'CPU Weight', + N'MONEY', + N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' - SET @StringToExecute += N'SELECT - 12 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Uncompressed backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' uncompressed backups in the last 30 days. This is a free way to save time and space. And SPACETIME. If your version of SQL supports it.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE backup_size = compressed_backup_size AND type = ''D'' - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; + UNION ALL + SELECT N'Total Duration', + N'BIGINT', + N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'Avg Duration', + N'BIGINT', + N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; + UNION ALL + SELECT N'Duration Weight', + N'MONEY', + N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' -RAISERROR('Rules analysis starting on temp tables', 0, 1) WITH NOWAIT; + UNION ALL + SELECT N'Total Reads', + N'BIGINT', + N'Total logical reads performed by this query since last compilation.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Diffs' AS [Finding], - 'On average, Differential backups for this database are >=40% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigDiff = 1 + UNION ALL + SELECT N'Average Reads', + N'BIGINT', + N'Average logical reads performed by each execution of this query since the last compilation.' - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Logs' AS [Finding], - 'On average, Log backups for this database are >=20% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigLog = 1 + UNION ALL + SELECT N'Read Weight', + N'MONEY', + N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' + UNION ALL + SELECT N'Total Writes', + N'BIGINT', + N'Total logical writes performed by this query since last compilation.' + UNION ALL + SELECT N'Average Writes', + N'BIGINT', + N'Average logical writes performed by each execution this query since last compilation.' -/*Insert thank you stuff last*/ - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) + UNION ALL + SELECT N'Write Weight', + N'MONEY', + N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' - SELECT - 2147483647 AS [CheckId], - 2147483647 AS [Priority], - 'From Your Community Volunteers' AS [DatabaseName], - 'sp_BlitzBackups Version: ' + @Version + ', Version Date: ' + CONVERT(VARCHAR(30), @VersionDate) + '.' AS [Finding], - 'Thanks for using our stored procedure. We hope you find it useful! Check out our other free SQL Server scripts at firstresponderkit.org!' AS [Warning]; + UNION ALL + SELECT N'Query Type', + N'NVARCHAR(258)', + N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' -RAISERROR('Rules analysis finished', 0, 1) WITH NOWAIT; + UNION ALL + SELECT N'Query Text', + N'NVARCHAR(4000)', + N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' -SELECT w.CheckId, w.Priority, w.DatabaseName, w.Finding, w.Warning -FROM #Warnings AS w -ORDER BY w.Priority, w.CheckId; + UNION ALL + SELECT N'% Executions (Type)', + N'MONEY', + N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' -DROP TABLE #Backups, #Warnings, #Recoverability, #RTORecoveryPoints + UNION ALL + SELECT N'% CPU (Type)', + N'MONEY', + N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' + UNION ALL + SELECT N'% Duration (Type)', + N'MONEY', + N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' -RETURN; + UNION ALL + SELECT N'% Reads (Type)', + N'MONEY', + N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' -PushBackupHistoryToListener: + UNION ALL + SELECT N'% Writes (Type)', + N'MONEY', + N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' -RAISERROR('Pushing backup history to listener', 0, 1) WITH NOWAIT; + UNION ALL + SELECT N'Total Rows', + N'BIGINT', + N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' -DECLARE @msg NVARCHAR(4000) = N''; -DECLARE @RemoteCheck TABLE (c INT NULL); + UNION ALL + SELECT N'Average Rows', + N'MONEY', + N'Average number of rows returned by each execution of the query.' + UNION ALL + SELECT N'Min Rows', + N'BIGINT', + N'The minimum number of rows returned by any execution of this query.' -IF @WriteBackupsToDatabaseName IS NULL - BEGIN - RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 0, 1) WITH NOWAIT - RETURN; - END + UNION ALL + SELECT N'Max Rows', + N'BIGINT', + N'The maximum number of rows returned by any execution of this query.' -IF LOWER(@WriteBackupsToDatabaseName) = N'msdb' - BEGIN - RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 0, 1) WITH NOWAIT - RETURN; - END + UNION ALL + SELECT N'MinGrantKB', + N'BIGINT', + N'The minimum memory grant the query received in kb.' -IF @WriteBackupsToListenerName IS NULL -BEGIN - IF @AGName IS NULL - BEGIN - RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 0, 1) WITH NOWAIT; - RETURN; - END - ELSE - BEGIN - SELECT @WriteBackupsToListenerName = dns_name - FROM sys.availability_groups AS ag - JOIN sys.availability_group_listeners AS agl - ON ag.group_id = agl.group_id - WHERE name = @AGName; - END + UNION ALL + SELECT N'MaxGrantKB', + N'BIGINT', + N'The maximum memory grant the query received in kb.' -END + UNION ALL + SELECT N'MinUsedGrantKB', + N'BIGINT', + N'The minimum used memory grant the query received in kb.' -IF @WriteBackupsToListenerName IS NOT NULL -BEGIN - IF NOT EXISTS - ( - SELECT * - FROM sys.servers s - WHERE name = @WriteBackupsToListenerName - ) - BEGIN - SET @msg = N'We need a linked server to write data across. Please set one up for ' + @WriteBackupsToListenerName + N'.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - RETURN; - END -END + UNION ALL + SELECT N'MaxUsedGrantKB', + N'BIGINT', + N'The maximum used memory grant the query received in kb.' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'MinSpills', + N'BIGINT', + N'The minimum amount this query has spilled to tempdb in 8k pages.' - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + N'.master.sys.databases d WHERE d.name = @i_WriteBackupsToDatabaseName;' + UNION ALL + SELECT N'MaxSpills', + N'BIGINT', + N'The maximum amount this query has spilled to tempdb in 8k pages.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'TotalSpills', + N'BIGINT', + N'The total amount this query has spilled to tempdb in 8k pages.' - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsToDatabaseName NVARCHAR(256)', @i_WriteBackupsToDatabaseName = @WriteBackupsToDatabaseName; + UNION ALL + SELECT N'AvgSpills', + N'BIGINT', + N'The average amount this query has spilled to tempdb in 8k pages.' - IF @@ROWCOUNT = 0 - BEGIN - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to exist on that server.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RETURN; - END + UNION ALL + SELECT N'PercentMemoryGrantUsed', + N'MONEY', + N'Result of dividing the maximum grant used by the minimum granted.' + UNION ALL + SELECT N'AvgMaxMemoryGrant', + N'MONEY', + N'The average maximum memory grant for a query.' - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'# Plans', + N'INT', + N'The total number of execution plans found that match a given query.' - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + '.' + QUOTENAME(@WriteBackupsToDatabaseName) + '.sys.tables WHERE name = ''backupset'' AND SCHEMA_NAME(schema_id) = ''dbo''; - ' + @crlf; + UNION ALL + SELECT N'# Distinct Plans', + N'INT', + N'The number of distinct execution plans that match a given query. ' + + NCHAR(13) + NCHAR(10) + + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' - IF @Debug = 1 - PRINT @StringToExecute; + UNION ALL + SELECT N'Created At', + N'DATETIME', + N'Time that the execution plan was last compiled.' - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute; + UNION ALL + SELECT N'Last Execution', + N'DATETIME', + N'The last time that this query was executed.' - IF @@ROWCOUNT = 0 - BEGIN + UNION ALL + SELECT N'Query Plan', + N'XML', + N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to have a table called dbo.backupset in it.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RAISERROR('Don''t worry, we''ll create it for you!', 0, 1) WITH NOWAIT - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; + UNION ALL + SELECT N'Plan Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to the compiled plan this query is a part of.' - SET @StringToExecute += N'CREATE TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ( backup_set_id INT IDENTITY(1, 1), backup_set_uuid UNIQUEIDENTIFIER, media_set_id INT, first_family_number TINYINT, first_media_number SMALLINT, - last_family_number TINYINT, last_media_number SMALLINT, catalog_family_number TINYINT, catalog_media_number SMALLINT, position INT, expiration_date DATETIME, - software_vendor_id INT, name NVARCHAR(128), description NVARCHAR(255), user_name NVARCHAR(128), software_major_version TINYINT, software_minor_version TINYINT, - software_build_version SMALLINT, time_zone SMALLINT, mtf_minor_version TINYINT, first_lsn NUMERIC(25, 0), last_lsn NUMERIC(25, 0), checkpoint_lsn NUMERIC(25, 0), - database_backup_lsn NUMERIC(25, 0), database_creation_date DATETIME, backup_start_date DATETIME, backup_finish_date DATETIME, type CHAR(1), sort_order SMALLINT, - code_page SMALLINT, compatibility_level TINYINT, database_version INT, backup_size NUMERIC(20, 0), database_name NVARCHAR(128), server_name NVARCHAR(128), - machine_name NVARCHAR(128), flags INT, unicode_locale INT, unicode_compare_style INT, collation_name NVARCHAR(128), is_password_protected BIT, recovery_model NVARCHAR(60), - has_bulk_logged_data BIT, is_snapshot BIT, is_readonly BIT, is_single_user BIT, has_backup_checksums BIT, is_damaged BIT, begins_log_chain BIT, has_incomplete_metadata BIT, - is_force_offline BIT, is_copy_only BIT, first_recovery_fork_guid UNIQUEIDENTIFIER, last_recovery_fork_guid UNIQUEIDENTIFIER, fork_point_lsn NUMERIC(25, 0), database_guid UNIQUEIDENTIFIER, - family_guid UNIQUEIDENTIFIER, differential_base_lsn NUMERIC(25, 0), differential_base_guid UNIQUEIDENTIFIER, compressed_backup_size NUMERIC(20, 0), key_algorithm NVARCHAR(32), - encryptor_thumbprint VARBINARY(20) , encryptor_type NVARCHAR(32) - ); - ' + @crlf; - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute + UNION ALL + SELECT N'SQL Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' + UNION ALL + SELECT N'Query Hash', + N'BINARY(8)', + N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' - RAISERROR('We''ll even make the indexes!', 0, 1) WITH NOWAIT + UNION ALL + SELECT N'Warnings', + N'VARCHAR(MAX)', + N'A list of individual warnings generated by this query.' ; - /*Checking for and creating the PK/CX*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - - IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name LIKE ? - ) + + /* Configuration table description */ + SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , + N'100' AS [Default Value] , + N'Executions / Minute' AS [Unit of Measure] , + N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] - BEGIN - ALTER TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ADD PRIMARY KEY CLUSTERED ([backup_set_id] ASC) - END - ' + UNION ALL + SELECT N'Parameter Sniffing Variance Percent' , + N'30' , + N'Percent' , + N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''PK[_][_]%'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute + UNION ALL + SELECT N'Parameter Sniffing IO Threshold' , + N'100,000' , + N'Logical reads' , + N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' + UNION ALL + SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' + UNION ALL + SELECT N'Long Running Query Warning' AS [Configuration Parameter] , + N'300' , + N'Seconds' , + N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' - /*Checking for and creating index on backup_set_uuid*/ + UNION ALL + SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; + RETURN; + END; /* IF @Help = 1 */ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - BEGIN - CREATE NONCLUSTERED INDEX [backupsetuuid] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_set_uuid] ASC) - END - ' - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetuuid'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on media_set_id*/ +/*Validate version*/ +IF ( +SELECT + CASE + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 + ELSE 1 + END +) = 0 +BEGIN + DECLARE @version_msg VARCHAR(8000); + SELECT @version_msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); + PRINT @version_msg; + RETURN; +END; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += 'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetMediaSetId] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([media_set_id] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetMediaSetId'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on backup_finish_date*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; +END; - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDate] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_finish_date] ASC) - END - ' +IF(@OutputType = 'NONE') +BEGIN + SET @HideSummary = 1; +END; - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDate'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute +/* Lets get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = LOWER(@SortOrder); - - /*Checking for and creating index on database_name*/ +/* Set @Top based on sort */ +IF ( + @Top IS NULL + AND @SortOrder IN ( 'all', 'all sort' ) + ) + BEGIN + SET @Top = 5; + END; - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) +IF ( + @Top IS NULL + AND @SortOrder NOT IN ( 'all', 'all sort' ) + ) + BEGIN + SET @Top = 10; + END; - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDatabaseName] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([database_name] ASC ) INCLUDE ([backup_set_id], [media_set_id]) - END - ' +/* If they want to sort by query hash, populate the @OnlyQueryHashes list for them */ +IF @SortOrder LIKE 'query hash%' + BEGIN + RAISERROR('Beginning query hash sort', 0, 1) WITH NOWAIT; + + SELECT TOP(@Top) qs.query_hash, + MAX(qs.max_worker_time) AS max_worker_time, + COUNT_BIG(*) AS records + INTO #query_hash_grouped + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY ( SELECT pa.value + FROM sys.dm_exec_plan_attributes(qs.plan_handle) AS pa + WHERE pa.attribute = 'dbid' ) AS ca + GROUP BY qs.query_hash, ca.value + HAVING COUNT_BIG(*) > 1 + ORDER BY max_worker_time DESC, + records DESC; + + SELECT TOP (1) + @OnlyQueryHashes = STUFF((SELECT DISTINCT N',' + CONVERT(NVARCHAR(MAX), qhg.query_hash, 1) + FROM #query_hash_grouped AS qhg + WHERE qhg.query_hash <> 0x00 + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + OPTION(RECOMPILE); - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDatabaseName'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' + /* When they ran it, @SortOrder probably looked like 'query hash, cpu', so strip the first sort order out: */ + SELECT @SortOrder = LTRIM(REPLACE(REPLACE(@SortOrder,'query hash', ''), ',', '')); - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - RAISERROR('Table and indexes created! You''re welcome!', 0, 1) WITH NOWAIT - END + /* If they just called it with @SortOrder = 'query hash', set it to 'cpu' for backwards compatibility: */ + IF @SortOrder = '' SET @SortOrder = 'cpu'; + END - RAISERROR('Beginning inserts', 0, 1) WITH NOWAIT; - RAISERROR(@crlf, 0, 1) WITH NOWAIT; - /* - Batching code comes from the lovely and talented Michael J. Swart - http://michaeljswart.com/2014/09/take-care-when-scripting-batches/ - If you're ever in Canada, he says you can stay at his house, too. - */ +/* If they want to sort by duplicate, create a table with the worst offenders - issue #3345 */ +IF @SortOrder LIKE 'duplicate%' + BEGIN + RAISERROR('Beginning duplicate query hash sort', 0, 1) WITH NOWAIT; + + /* Find the query hashes that are the most duplicated */ + WITH MostCommonQueries AS ( + SELECT TOP(@Top) qs.query_hash, + COUNT_BIG(*) AS plans + FROM sys.dm_exec_query_stats AS qs + GROUP BY qs.query_hash + HAVING COUNT_BIG(*) > 100 + ORDER BY COUNT_BIG(*) DESC + ) + SELECT mcq_recent.sql_handle, mcq_recent.plan_handle, mcq_recent.creation_time AS duplicate_creation_time, mcq.plans + INTO #duplicate_query_filter + FROM MostCommonQueries mcq + CROSS APPLY ( SELECT TOP 1 qs.sql_handle, qs.plan_handle, qs.creation_time + FROM sys.dm_exec_query_stats qs + WHERE qs.query_hash = mcq.query_hash + ORDER BY qs.creation_time DESC) AS mcq_recent + OPTION (RECOMPILE); + SET @MinimumExecutionCount = 0; + END - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - SET @StringToExecute += N' - DECLARE - @StartDate DATETIME = DATEADD(HOUR, @i_WriteBackupsLastHours, SYSDATETIME()), - @StartDateNext DATETIME, - @RC INT = 1, - @msg NVARCHAR(4000) = N''''; - - SELECT @StartDate = MIN(b.backup_start_date) - FROM msdb.dbo.backupset b - WHERE b.backup_start_date >= @StartDate - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) +/* validate user inputs */ +IF @Top IS NULL + OR @SortOrder IS NULL + OR @QueryFilter IS NULL + OR @Reanalyze IS NULL +BEGIN + RAISERROR(N'Several parameters (@Top, @SortOrder, @QueryFilter, @renalyze) are required. Do not set them to NULL. Please try again.', 16, 1) WITH NOWAIT; + RETURN; +END; - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); +RAISERROR(N'Checking @MinutesBack validity.', 0, 1) WITH NOWAIT; +IF @MinutesBack IS NOT NULL + BEGIN + IF @MinutesBack > 0 + BEGIN + RAISERROR(N'Setting @MinutesBack to a negative number', 0, 1) WITH NOWAIT; + SET @MinutesBack *=-1; + END; + IF @MinutesBack = 0 + BEGIN + RAISERROR(N'@MinutesBack can''t be 0, setting to -1', 0, 1) WITH NOWAIT; + SET @MinutesBack = -1; + END; + END; - IF - ( @StartDate IS NULL ) - BEGIN - SET @msg = N''No data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RETURN; - END - RAISERROR(''Starting insert loop'', 0, 1) WITH NOWAIT; +DECLARE @DurationFilter_i INT, + @MinMemoryPerQuery INT, + @msg NVARCHAR(4000), + @NoobSaibot BIT = 0, + @VersionShowsAirQuoteActualPlans BIT, + @ObjectFullName NVARCHAR(2000), + @user_perm_sql NVARCHAR(MAX) = N'', + @user_perm_gb_out DECIMAL(10,2), + @common_version DECIMAL(10,2), + @buffer_pool_memory_gb DECIMAL(10,2), + @user_perm_percent DECIMAL(10,2), + @is_tokenstore_big BIT = 0, + @sort NVARCHAR(MAX) = N'', + @sort_filter NVARCHAR(MAX) = N''; + + +IF @SortOrder = 'sp_BlitzIndex' +BEGIN + RAISERROR(N'OUTSTANDING!', 0, 1) WITH NOWAIT; + SET @SortOrder = 'reads'; + SET @NoobSaibot = 1; - WHILE EXISTS ( - SELECT 1 - FROM msdb.dbo.backupset b - WHERE NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) - ) - BEGIN - - SET @msg = N''Inserting data for '' + CONVERT(NVARCHAR(30), @StartDate) + '' through '' + + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - ' +END - SET @StringToExecute += N'INSERT ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ' - SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data)' + @crlf - ELSE + N'has_bulk_logged_data)' + @crlf - END - - SET @StringToExecute +=N' - SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data' + @crlf - ELSE + N'has_bulk_logged_data' + @crlf - END - SET @StringToExecute +=N' - FROM msdb.dbo.backupset b - WHERE 1=1 - AND b.backup_start_date >= @StartDate - AND b.backup_start_date < @StartDateNext - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - )' + @crlf; +/* Change duration from seconds to milliseconds */ +IF @DurationFilter IS NOT NULL + BEGIN + RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; + SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); + END; - SET @StringToExecute +=N' - SET @RC = @@ROWCOUNT; - - SET @msg = N''Inserted '' + CONVERT(NVARCHAR(30), @RC) + '' rows for ''+ CONVERT(NVARCHAR(30), @StartDate) + '' through '' + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - SET @StartDate = @StartDateNext; - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); +RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; +SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; - IF - ( @StartDate > SYSDATETIME() ) - BEGIN - - SET @msg = N''No more data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - BREAK; +IF SERVERPROPERTY('EngineEdition') IN (5, 6) AND DB_NAME() <> @DatabaseName +BEGIN + RAISERROR('You specified a database name other than the current database, but Azure SQL DB does not allow you to change databases. Execute sp_BlitzCache from the database you want to analyze.', 16, 1); + RETURN; +END; +IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> N'' +BEGIN + RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); + RETURN; +END; +IF (SELECT DATABASEPROPERTYEX(ISNULL(@DatabaseName, 'master'), 'Collation')) IS NULL AND SERVERPROPERTY('EngineEdition') NOT IN (5, 6, 8) +BEGIN + RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); + RETURN; +END; - END - END' + @crlf; +SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; - IF @Debug = 1 - PRINT @StringToExecute; +SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsLastHours INT', @i_WriteBackupsLastHours = @WriteBackupsLastHours; +SET @SortOrder = CASE + WHEN @SortOrder IN ('executions per minute','execution per minute','executions / minute','execution / minute','xpm') THEN 'avg executions' + WHEN @SortOrder IN ('recent compilations','recent compilation','compile') THEN 'compiles' + WHEN @SortOrder IN ('read') THEN 'reads' + WHEN @SortOrder IN ('avg read') THEN 'avg reads' + WHEN @SortOrder IN ('write') THEN 'writes' + WHEN @SortOrder IN ('avg write') THEN 'avg writes' + WHEN @SortOrder IN ('memory grants') THEN 'memory grant' + WHEN @SortOrder IN ('avg memory grants') THEN 'avg memory grant' + WHEN @SortOrder IN ('unused grants','unused memory', 'unused memory grant', 'unused memory grants') THEN 'unused grant' + WHEN @SortOrder IN ('spill') THEN 'spills' + WHEN @SortOrder IN ('avg spill') THEN 'avg spills' + WHEN @SortOrder IN ('execution') THEN 'executions' + WHEN @SortOrder IN ('duplicates') THEN 'duplicate' + ELSE @SortOrder END + +RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; +IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', + 'duration', 'avg duration', 'executions', 'avg executions', + 'compiles', 'memory grant', 'avg memory grant', 'unused grant', + 'spills', 'avg spills', 'all', 'all avg', 'sp_BlitzIndex', + 'query hash', 'duplicate') + BEGIN + RAISERROR(N'Invalid sort order chosen, reverting to cpu', 16, 1) WITH NOWAIT; + SET @SortOrder = 'cpu'; + END; -END; +SET @QueryFilter = LOWER(@QueryFilter); -END; +IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') + BEGIN + RAISERROR(N'Invalid query filter chosen. Reverting to all.', 0, 1) WITH NOWAIT; + SET @QueryFilter = 'all'; + END; -GO -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO +IF @SkipAnalysis = 1 + BEGIN + RAISERROR(N'Skip Analysis set to 1, hiding Summary', 0, 1) WITH NOWAIT; + SET @HideSummary = 1; + END; -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @msg VARCHAR(8000); - SELECT @msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; -END; +DECLARE @AllSortSql NVARCHAR(MAX) = N''; +DECLARE @VersionShowsMemoryGrants BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb') + SET @VersionShowsMemoryGrants = 1; +ELSE + SET @VersionShowsMemoryGrants = 0; -IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); -GO +DECLARE @VersionShowsSpills BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills') + SET @VersionShowsSpills = 1; +ELSE + SET @VersionShowsSpills = 0; -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheProcs;'); -GO +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_plan_stats') AND name = 'query_plan') + SET @VersionShowsAirQuoteActualPlans = 1; +ELSE + SET @VersionShowsAirQuoteActualPlans = 0; -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheResults;'); -GO +IF @Reanalyze = 1 + BEGIN + IF OBJECT_ID('tempdb..##BlitzCacheResults') IS NULL + BEGIN + RAISERROR(N'##BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; + SET @Reanalyze = 0; + END + ELSE + BEGIN + RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; + GOTO Results; + END; + END; + + +IF @SortOrder IN ('all', 'all avg') + BEGIN + RAISERROR(N'Checking all sort orders, please be patient', 0, 1) WITH NOWAIT; + GOTO AllSorts; + END; + +RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL + DROP TABLE #only_query_hashes ; + +IF OBJECT_ID('tempdb..#ignore_query_hashes') IS NOT NULL + DROP TABLE #ignore_query_hashes ; + +IF OBJECT_ID('tempdb..#only_sql_handles') IS NOT NULL + DROP TABLE #only_sql_handles ; + +IF OBJECT_ID('tempdb..#ignore_sql_handles') IS NOT NULL + DROP TABLE #ignore_sql_handles ; + +IF OBJECT_ID('tempdb..#p') IS NOT NULL + DROP TABLE #p; + +IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL + DROP TABLE #checkversion; + +IF OBJECT_ID ('tempdb..#configuration') IS NOT NULL + DROP TABLE #configuration; + +IF OBJECT_ID ('tempdb..#stored_proc_info') IS NOT NULL + DROP TABLE #stored_proc_info; + +IF OBJECT_ID ('tempdb..#plan_creation') IS NOT NULL + DROP TABLE #plan_creation; + +IF OBJECT_ID ('tempdb..#est_rows') IS NOT NULL + DROP TABLE #est_rows; + +IF OBJECT_ID ('tempdb..#plan_cost') IS NOT NULL + DROP TABLE #plan_cost; + +IF OBJECT_ID ('tempdb..#proc_costs') IS NOT NULL + DROP TABLE #proc_costs; + +IF OBJECT_ID ('tempdb..#stats_agg') IS NOT NULL + DROP TABLE #stats_agg; + +IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL + DROP TABLE #trace_flags; + +IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL + DROP TABLE #variable_info; + +IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL + DROP TABLE #conversion_info; + +IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL + DROP TABLE #missing_index_xml; + +IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL + DROP TABLE #missing_index_schema; + +IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL + DROP TABLE #missing_index_usage; + +IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL + DROP TABLE #missing_index_detail; + +IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL + DROP TABLE #missing_index_pretty; + +IF OBJECT_ID('tempdb..#index_spool_ugly') IS NOT NULL + DROP TABLE #index_spool_ugly; + +IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + +IF OBJECT_ID('tempdb..#plan_usage') IS NOT NULL + DROP TABLE #plan_usage; + +CREATE TABLE #only_query_hashes ( + query_hash BINARY(8) +); + +CREATE TABLE #ignore_query_hashes ( + query_hash BINARY(8) +); + +CREATE TABLE #only_sql_handles ( + sql_handle VARBINARY(64) +); + +CREATE TABLE #ignore_sql_handles ( + sql_handle VARBINARY(64) +); -CREATE TABLE ##bou_BlitzCacheResults ( +CREATE TABLE #p ( + SqlHandle VARBINARY(64), + TotalCPU BIGINT, + TotalDuration BIGINT, + TotalReads BIGINT, + TotalWrites BIGINT, + ExecutionCount BIGINT +); + +CREATE TABLE #checkversion ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) +); + +CREATE TABLE #configuration ( + parameter_name VARCHAR(100), + value DECIMAL(38,0) +); + +CREATE TABLE #plan_creation +( + percent_24 DECIMAL(5, 2), + percent_4 DECIMAL(5, 2), + percent_1 DECIMAL(5, 2), + total_plans INT, + SPID INT +); + +CREATE TABLE #est_rows +( + QueryHash BINARY(8), + estimated_rows FLOAT +); + +CREATE TABLE #plan_cost +( + QueryPlanCost FLOAT, + SqlHandle VARBINARY(64), + PlanHandle VARBINARY(64), + QueryHash BINARY(8), + QueryPlanHash BINARY(8) +); + +CREATE TABLE #proc_costs +( + PlanTotalQuery FLOAT, + PlanHandle VARBINARY(64), + SqlHandle VARBINARY(64) +); + +CREATE TABLE #stats_agg +( + SqlHandle VARBINARY(64), + LastUpdate DATETIME2(7), + ModificationCount BIGINT, + SamplingPercent FLOAT, + [Statistics] NVARCHAR(258), + [Table] NVARCHAR(258), + [Schema] NVARCHAR(258), + [Database] NVARCHAR(258), +); + +CREATE TABLE #trace_flags +( + SqlHandle VARBINARY(64), + QueryHash BINARY(8), + global_trace_flags VARCHAR(1000), + session_trace_flags VARCHAR(1000) +); + +CREATE TABLE #stored_proc_info +( SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) + SqlHandle VARBINARY(64), + QueryHash BINARY(8), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + converted_column_name NVARCHAR(258), + compile_time_value NVARCHAR(258), + proc_name NVARCHAR(1000), + column_name NVARCHAR(4000), + converted_to NVARCHAR(258), + set_options NVARCHAR(1000) ); -CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - /*The Memory Grant columns are only supported - in certain versions, giggle giggle. - */ - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); -GO - -ALTER PROCEDURE dbo.sp_BlitzCache - @Help BIT = 0, - @Top INT = NULL, - @SortOrder VARCHAR(50) = 'CPU', - @UseTriggersAnyway BIT = NULL, - @ExportToExcel BIT = 0, - @ExpertMode TINYINT = 0, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @ConfigurationDatabaseName NVARCHAR(128) = NULL , - @ConfigurationSchemaName NVARCHAR(256) = NULL , - @ConfigurationTableName NVARCHAR(256) = NULL , - @DurationFilter DECIMAL(38,4) = NULL , - @HideSummary BIT = 0 , - @IgnoreSystemDBs BIT = 1 , - @OnlyQueryHashes VARCHAR(MAX) = NULL , - @IgnoreQueryHashes VARCHAR(MAX) = NULL , - @OnlySqlHandles VARCHAR(MAX) = NULL , - @IgnoreSqlHandles VARCHAR(MAX) = NULL , - @QueryFilter VARCHAR(10) = 'ALL' , - @DatabaseName NVARCHAR(128) = NULL , - @StoredProcName NVARCHAR(128) = NULL, - @Reanalyze BIT = 0 , - @SkipAnalysis BIT = 0 , - @BringThePain BIT = 0, /* This will forcibly set @Top to 2,147,483,647 */ - @MinimumExecutionCount INT = 0, - @Debug BIT = 0, - @CheckDateOverride DATETIMEOFFSET = NULL, - @MinutesBack INT = NULL, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - -IF @Help = 1 PRINT ' -sp_BlitzCache from http://FirstResponderKit.org - -This script displays your most resource-intensive queries from the plan cache, -and points to ways you can tune these queries to make them faster. - - -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. - -Known limitations of this version: - - This query will not run on SQL Server 2005. - - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is - excluded by default. - - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes - with no spaces between the hash values. - - @OutputServerName is not functional yet. - -Unknown limitations of this version: - - May or may not be vulnerable to the wick effect. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - -MIT License +CREATE TABLE #variable_info +( + SPID INT, + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + proc_name NVARCHAR(1000), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + compile_time_value NVARCHAR(258) +); -Copyright (c) 2016 Brent Ozar Unlimited +CREATE TABLE #conversion_info +( + SPID INT, + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + proc_name NVARCHAR(258), + expression NVARCHAR(4000), + at_charindex AS CHARINDEX('@', expression), + bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), + comma_charindex AS CHARINDEX(',', expression) + 1, + second_comma_charindex AS + CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, + equal_charindex AS CHARINDEX('=', expression) + 1, + paren_charindex AS CHARINDEX('(', expression) + 1, + comma_paren_charindex AS + CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, + convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression) +); -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. +CREATE TABLE #missing_index_xml +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + index_xml XML +); -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. -'; -DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; +CREATE TABLE #missing_index_schema +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + index_xml XML +); -IF @Help = 1 -BEGIN - SELECT N'@Help' AS [Parameter Name] , - N'BIT' AS [Data Type] , - N'Displays this help message.' AS [Parameter Description] - UNION ALL - SELECT N'@Top', - N'INT', - N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' +CREATE TABLE #missing_index_usage +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + usage NVARCHAR(128), + index_xml XML +); - UNION ALL - SELECT N'@SortOrder', - N'VARCHAR(10)', - N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' - UNION ALL - SELECT N'@UseTriggersAnyway', - N'BIT', - N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' +CREATE TABLE #missing_index_detail +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + usage NVARCHAR(128), + column_name NVARCHAR(128) +); - UNION ALL - SELECT N'@ExportToExcel', - N'BIT', - N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' - UNION ALL - SELECT N'@ExpertMode', - N'TINYINT', - N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' - - UNION ALL - SELECT N'@OutputDatabaseName', - N'NVARCHAR(128)', - N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputSchemaName', - N'NVARCHAR(256)', - N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputTableName', - N'NVARCHAR(256)', - N'The output table. If this does not exist, it will be created for you.' - - UNION ALL - SELECT N'@DurationFilter', - N'DECIMAL(38,4)', - N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' +CREATE TABLE #missing_index_pretty +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128), + is_spool BIT, + details AS N'/* ' + + CHAR(10) + + CASE is_spool + WHEN 0 + THEN N'The Query Processor estimates that implementing the ' + ELSE N'We estimate that implementing the ' + END + + N'following index could improve query cost (' + query_cost + N')' + + CHAR(10) + + N'by ' + + CONVERT(NVARCHAR(30), impact) + + N'% for ' + executions + N' executions of the query' + + N' over the last ' + + CASE WHEN creation_hours < 24 + THEN creation_hours + N' hours.' + WHEN creation_hours = 24 + THEN ' 1 day.' + WHEN creation_hours > 24 + THEN (CONVERT(NVARCHAR(128), creation_hours / 24)) + N' days.' + ELSE N'' + END + + CHAR(10) + + N'*/' + + CHAR(10) + CHAR(13) + + N'/* ' + + CHAR(10) + + N'USE ' + + database_name + + CHAR(10) + + N'GO' + + CHAR(10) + CHAR(13) + + N'CREATE NONCLUSTERED INDEX ix_' + + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') + + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') + + CASE WHEN [include] IS NOT NULL THEN + N'_Includes' ELSE N'' END + + CHAR(10) + + N' ON ' + + schema_name + + N'.' + + table_name + + N' (' + + + CASE WHEN equality IS NOT NULL + THEN equality + + CASE WHEN inequality IS NOT NULL + THEN N', ' + inequality + ELSE N'' + END + ELSE inequality + END + + N')' + + CHAR(10) + + CASE WHEN include IS NOT NULL + THEN N'INCLUDE (' + include + N') WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + ELSE N' WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + END + + CHAR(10) + + N'GO' + + CHAR(10) + + N'*/' +); - UNION ALL - SELECT N'@HideSummary', - N'BIT', - N'Hides the findings summary result set.' - UNION ALL - SELECT N'@IgnoreSystemDBs', - N'BIT', - N'Ignores plans found in the system databases (master, model, msdb, tempdb, and resourcedb)' +CREATE TABLE #index_spool_ugly +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128) +); - UNION ALL - SELECT N'@OnlyQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' - UNION ALL - SELECT N'@IgnoreQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to ignore.' - - UNION ALL - SELECT N'@OnlySqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to use for filtering results.' - - UNION ALL - SELECT N'@IgnoreSqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to ignore.' +CREATE TABLE #ReadableDBs +( +database_id INT +); - UNION ALL - SELECT N'@DatabaseName', - N'NVARCHAR(128)', - N'A database name which is used for filtering results.' - UNION ALL - SELECT N'@StoredProcName', - N'NVARCHAR(128)', - N'Name of stored procedure you want to find plans for.' +CREATE TABLE #plan_usage +( + duplicate_plan_hashes BIGINT NULL, + percent_duplicate DECIMAL(9, 2) NULL, + single_use_plan_count BIGINT NULL, + percent_single DECIMAL(9, 2) NULL, + total_plans BIGINT NULL, + spid INT +); - UNION ALL - SELECT N'@BringThePain', - N'BIT', - N'This forces sp_BlitzCache to examine the entire plan cache. Be careful running this on servers with a lot of memory or a large execution plan cache.' - UNION ALL - SELECT N'@QueryFilter', - N'VARCHAR(10)', - N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' +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; - UNION ALL - SELECT N'@Reanalyze', - N'BIT', - N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' - - UNION ALL - SELECT N'@MinimumExecutionCount', - N'INT', - N'Queries with fewer than this number of executions will be omitted from results.' - - UNION ALL - SELECT N'@Debug', - N'BIT', - N'Setting this to 1 will print dynamic SQL and select data from all tables used.' + EXEC('INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME OPTION (RECOMPILE);'); + EXEC('INSERT INTO #ReadableDBs VALUES (32767) ;'); -- Exclude internal resource database as well +END - UNION ALL - SELECT N'@MinutesBack', - N'INT', - N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negtive.'; +RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; +WITH x AS ( +SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], + SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 4 THEN 1 ELSE 0 END) AS [plans_4], + SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 1 THEN 1 ELSE 0 END) AS [plans_1], + COUNT(deqs.creation_time) AS [total_plans] +FROM sys.dm_exec_query_stats AS deqs +) +INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) +SELECT CONVERT(DECIMAL(5,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], + x.total_plans, + @@SPID AS SPID +FROM x +OPTION (RECOMPILE); - /* Column definitions */ - SELECT N'# Executions' AS [Column Name], - N'BIGINT' AS [Data Type], - N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] +RAISERROR(N'Checking for single use plans and plans with many queries', 0, 1) WITH NOWAIT; +WITH total_plans AS +( + SELECT + COUNT_BIG(deqs.query_plan_hash) AS total_plans + FROM sys.dm_exec_query_stats AS deqs +), + many_plans AS +( + SELECT + SUM(x.duplicate_plan_hashes) AS duplicate_plan_hashes + FROM + ( + SELECT + COUNT_BIG(qs.query_plan_hash) AS duplicate_plan_hashes + FROM sys.dm_exec_query_stats qs + LEFT JOIN sys.dm_exec_procedure_stats ps ON qs.plan_handle = ps.plan_handle + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = N'dbid' + AND pa.value <> 32767 /*Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/ + AND qs.query_plan_hash <> 0x0000000000000000 + GROUP BY + /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */ + qs.query_hash, + ps.object_id, + pa.value + HAVING COUNT_BIG(qs.query_plan_hash) > 5 + ) AS x +), + single_use_plans AS +( + SELECT + COUNT_BIG(*) AS single_use_plan_count + FROM sys.dm_exec_query_stats AS s + WHERE s.execution_count = 1 +) +INSERT + #plan_usage +( + duplicate_plan_hashes, + percent_duplicate, + single_use_plan_count, + percent_single, + total_plans, + spid +) +SELECT + m.duplicate_plan_hashes, + CONVERT + ( + decimal(5,2), + m.duplicate_plan_hashes + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_duplicate, + s.single_use_plan_count, + CONVERT + ( + decimal(5,2), + s.single_use_plan_count + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_single, + t.total_plans, + @@SPID +FROM many_plans AS m +CROSS JOIN single_use_plans AS s +CROSS JOIN total_plans AS t; - UNION ALL - SELECT N'Executions / Minute', - N'MONEY', - N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' - UNION ALL - SELECT N'Execution Weight', - N'MONEY', - N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' +/* +Erik Darling: + Quoting this out to see if the above query fixes the issue + 2021-05-17, Issue #2909 - UNION ALL - SELECT N'Database', - N'sysname', - N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' +UPDATE #plan_usage + SET percent_duplicate = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END, + percent_single = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END; +*/ - UNION ALL - SELECT N'Total CPU', - N'BIGINT', - N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' +SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; +SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; +SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; - UNION ALL - SELECT N'Avg CPU', - N'BIGINT', - N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' +DECLARE @individual VARCHAR(100) ; - UNION ALL - SELECT N'CPU Weight', - N'MONEY', - N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' +IF (@OnlySqlHandles IS NOT NULL AND @IgnoreSqlHandles IS NOT NULL) +BEGIN +RAISERROR('You shouldn''t need to ignore and filter on SqlHandle at the same time.', 0, 1) WITH NOWAIT; +RETURN; +END; - UNION ALL - SELECT N'Total Duration', - N'BIGINT', - N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' +IF (@StoredProcName IS NOT NULL AND (@OnlySqlHandles IS NOT NULL OR @IgnoreSqlHandles IS NOT NULL)) +BEGIN +RAISERROR('You can''t filter on stored procedure name and SQL Handle.', 0, 1) WITH NOWAIT; +RETURN; +END; - UNION ALL - SELECT N'Avg Duration', - N'BIGINT', - N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' +IF @OnlySqlHandles IS NOT NULL + AND LEN(@OnlySqlHandles) > 0 +BEGIN + RAISERROR(N'Processing SQL Handles', 0, 1) WITH NOWAIT; + SET @individual = ''; - UNION ALL - SELECT N'Duration Weight', - N'MONEY', - N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' + WHILE LEN(@OnlySqlHandles) > 0 + BEGIN + IF PATINDEX('%,%', @OnlySqlHandles) > 0 + BEGIN + SET @individual = SUBSTRING(@OnlySqlHandles, 0, PATINDEX('%,%',@OnlySqlHandles)) ; + + INSERT INTO #only_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - UNION ALL - SELECT N'Total Reads', - N'BIGINT', - N'Total logical reads performed by this query since last compilation.' + SET @OnlySqlHandles = SUBSTRING(@OnlySqlHandles, LEN(@individual + ',') + 1, LEN(@OnlySqlHandles)) ; + END; + ELSE + BEGIN + SET @individual = @OnlySqlHandles; + SET @OnlySqlHandles = NULL; - UNION ALL - SELECT N'Average Reads', - N'BIGINT', - N'Average logical reads performed by each execution of this query since the last compilation.' + INSERT INTO #only_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; - UNION ALL - SELECT N'Read Weight', - N'MONEY', - N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; - UNION ALL - SELECT N'Total Writes', - N'BIGINT', - N'Total logical writes performed by this query since last compilation.' +IF @IgnoreSqlHandles IS NOT NULL + AND LEN(@IgnoreSqlHandles) > 0 +BEGIN + RAISERROR(N'Processing SQL Handles To Ignore', 0, 1) WITH NOWAIT; + SET @individual = ''; - UNION ALL - SELECT N'Average Writes', - N'BIGINT', - N'Average logical writes performed by each execution this query since last compilation.' + WHILE LEN(@IgnoreSqlHandles) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreSqlHandles) > 0 + BEGIN + SET @individual = SUBSTRING(@IgnoreSqlHandles, 0, PATINDEX('%,%',@IgnoreSqlHandles)) ; + + INSERT INTO #ignore_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - UNION ALL - SELECT N'Write Weight', - N'MONEY', - N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' + SET @IgnoreSqlHandles = SUBSTRING(@IgnoreSqlHandles, LEN(@individual + ',') + 1, LEN(@IgnoreSqlHandles)) ; + END; + ELSE + BEGIN + SET @individual = @IgnoreSqlHandles; + SET @IgnoreSqlHandles = NULL; - UNION ALL - SELECT N'Query Type', - N'NVARCHAR(256)', - N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' + INSERT INTO #ignore_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; - UNION ALL - SELECT N'Query Text', - N'NVARCHAR(4000)', - N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; - UNION ALL - SELECT N'% Executions (Type)', - N'MONEY', - N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' +IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' - UNION ALL - SELECT N'% CPU (Type)', - N'MONEY', - N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' +BEGIN + RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; + + DECLARE @function_search_sql NVARCHAR(MAX) = N'' + + INSERT #only_sql_handles + ( sql_handle ) + SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + FROM sys.dm_exec_procedure_stats AS deps + WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName UNION ALL - SELECT N'% Duration (Type)', - N'MONEY', - N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' + + SELECT ISNULL(dets.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + FROM sys.dm_exec_trigger_stats AS dets + WHERE OBJECT_NAME(dets.object_id, dets.database_id) = @StoredProcName + OPTION (RECOMPILE); - UNION ALL - SELECT N'% Reads (Type)', - N'MONEY', - N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' + IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_search_sql = @function_search_sql + N' + SELECT ISNULL(defs.sql_handle, CONVERT(VARBINARY(64),''0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'')) + FROM sys.dm_exec_function_stats AS defs + WHERE OBJECT_NAME(defs.object_id, defs.database_id) = @i_StoredProcName + OPTION (RECOMPILE); + ' + INSERT #only_sql_handles ( sql_handle ) + EXEC sys.sp_executesql @function_search_sql, N'@i_StoredProcName NVARCHAR(128)', @StoredProcName + END + + IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 + BEGIN + RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; + RETURN; + END; - UNION ALL - SELECT N'% Writes (Type)', - N'MONEY', - N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' +END; - UNION ALL - SELECT N'Total Rows', - N'BIGINT', - N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' - UNION ALL - SELECT N'Average Rows', - N'MONEY', - N'Average number of rows returned by each execution of the query.' - UNION ALL - SELECT N'Min Rows', - N'BIGINT', - N'The minimum number of rows returned by any execution of this query.' +IF ((@OnlyQueryHashes IS NOT NULL AND LEN(@OnlyQueryHashes) > 0) + OR (@IgnoreQueryHashes IS NOT NULL AND LEN(@IgnoreQueryHashes) > 0)) + AND LEFT(@QueryFilter, 3) IN ('pro', 'fun') +BEGIN + RAISERROR('You cannot limit by query hash and filter by stored procedure', 16, 1); + RETURN; +END; - UNION ALL - SELECT N'Max Rows', - N'BIGINT', - N'The maximum number of rows returned by any execution of this query.' +/* If the user is attempting to limit by query hash, set up the + #only_query_hashes temp table. This will be used to narrow down + results. - UNION ALL - SELECT N'MinGrantKB', - N'BIGINT', - N'The minimum memory grant the query received in kb.' + Just a reminder: Using @OnlyQueryHashes will ignore stored + procedures and triggers. + */ +IF @OnlyQueryHashes IS NOT NULL + AND LEN(@OnlyQueryHashes) > 0 +BEGIN + RAISERROR(N'Setting up filter for Query Hashes', 0, 1) WITH NOWAIT; + SET @individual = ''; - UNION ALL - SELECT N'MaxGrantKB', - N'BIGINT', - N'The maximum memory grant the query received in kb.' + WHILE LEN(@OnlyQueryHashes) > 0 + BEGIN + IF PATINDEX('%,%', @OnlyQueryHashes) > 0 + BEGIN + SET @individual = SUBSTRING(@OnlyQueryHashes, 0, PATINDEX('%,%',@OnlyQueryHashes)) ; + + INSERT INTO #only_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - UNION ALL - SELECT N'MinUsedGrantKB', - N'BIGINT', - N'The minimum used memory grant the query received in kb.' + SET @OnlyQueryHashes = SUBSTRING(@OnlyQueryHashes, LEN(@individual + ',') + 1, LEN(@OnlyQueryHashes)) ; + END; + ELSE + BEGIN + SET @individual = @OnlyQueryHashes; + SET @OnlyQueryHashes = NULL; - UNION ALL - SELECT N'MaxUsedGrantKB', - N'BIGINT', - N'The maximum used memory grant the query received in kb.' + INSERT INTO #only_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; - UNION ALL - SELECT N'PercentMemoryGrantUsed', - N'MONEY', - N'Result of dividing the maximum grant used by the minimum granted.' + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; - UNION ALL - SELECT N'AvgMaxMemoryGrant', - N'MONEY', - N'The average maximum memory grant for a query.' +/* If the user is setting up a list of query hashes to ignore, those + values will be inserted into #ignore_query_hashes. This is used to + exclude values from query results. - UNION ALL - SELECT N'# Plans', - N'INT', - N'The total number of execution plans found that match a given query.' + Just a reminder: Using @IgnoreQueryHashes will ignore stored + procedures and triggers. + */ +IF @IgnoreQueryHashes IS NOT NULL + AND LEN(@IgnoreQueryHashes) > 0 +BEGIN + RAISERROR(N'Setting up filter to ignore query hashes', 0, 1) WITH NOWAIT; + SET @individual = '' ; - UNION ALL - SELECT N'# Distinct Plans', - N'INT', - N'The number of distinct execution plans that match a given query. ' - + NCHAR(13) + NCHAR(10) - + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' + WHILE LEN(@IgnoreQueryHashes) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreQueryHashes) > 0 + BEGIN + SET @individual = SUBSTRING(@IgnoreQueryHashes, 0, PATINDEX('%,%',@IgnoreQueryHashes)) ; + + INSERT INTO #ignore_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + SET @IgnoreQueryHashes = SUBSTRING(@IgnoreQueryHashes, LEN(@individual + ',') + 1, LEN(@IgnoreQueryHashes)) ; + END; + ELSE + BEGIN + SET @individual = @IgnoreQueryHashes ; + SET @IgnoreQueryHashes = NULL ; - UNION ALL - SELECT N'Created At', - N'DATETIME', - N'Time that the execution plan was last compiled.' + INSERT INTO #ignore_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + END; + END; +END; - UNION ALL - SELECT N'Last Execution', - N'DATETIME', - N'The last time that this query was executed.' +IF @ConfigurationDatabaseName IS NOT NULL +BEGIN + RAISERROR(N'Reading values from Configuration Database', 0, 1) WITH NOWAIT; + DECLARE @config_sql NVARCHAR(MAX) = N'INSERT INTO #configuration SELECT parameter_name, value FROM ' + + QUOTENAME(@ConfigurationDatabaseName) + + '.' + QUOTENAME(@ConfigurationSchemaName) + + '.' + QUOTENAME(@ConfigurationTableName) + + ' ; ' ; + EXEC(@config_sql); +END; - UNION ALL - SELECT N'Query Plan', - N'XML', - N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' +RAISERROR(N'Setting up variables', 0, 1) WITH NOWAIT; +DECLARE @sql NVARCHAR(MAX) = N'', + @insert_list NVARCHAR(MAX) = N'', + @plans_triggers_select_list NVARCHAR(MAX) = N'', + @body NVARCHAR(MAX) = N'', + @body_where NVARCHAR(MAX) = N'WHERE 1 = 1 ' + @nl, + @body_order NVARCHAR(MAX) = N'ORDER BY #sortable# DESC OPTION (RECOMPILE) ', + + @q NVARCHAR(1) = N'''', + @pv VARCHAR(20), + @pos TINYINT, + @v DECIMAL(6,2), + @build INT; - UNION ALL - SELECT N'Plan Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to the compiled plan this query is a part of.' - UNION ALL - SELECT N'SQL Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' +RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; - UNION ALL - SELECT N'Query Hash', - N'BINARY(8)', - N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' +INSERT INTO #checkversion (version) +SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) +OPTION (RECOMPILE); - UNION ALL - SELECT N'Warnings', - N'VARCHAR(MAX)', - N'A list of individual warnings generated by this query.' ; +SELECT @v = common_version , + @build = build +FROM #checkversion +OPTION (RECOMPILE); - - /* Configuration table description */ - SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , - N'100' AS [Default Value] , - N'Executions / Minute' AS [Unit of Measure] , - N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] +IF (@SortOrder IN ('memory grant', 'avg memory grant')) AND @VersionShowsMemoryGrants = 0 +BEGIN + RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); + RETURN; +END; - UNION ALL - SELECT N'Parameter Sniffing Variance Percent' , - N'30' , - N'Percent' , - N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' +IF (@SortOrder IN ('spills', 'avg spills') AND @VersionShowsSpills = 0) +BEGIN + RAISERROR('Your version of SQL does not support sorting by spills. Please use another sort order.', 16, 1); + RETURN; +END; - UNION ALL - SELECT N'Parameter Sniffing IO Threshold' , - N'100,000' , - N'Logical reads' , - N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' +IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) +BEGIN + RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); + RETURN; +END; - UNION ALL - SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' +RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; - UNION ALL - SELECT N'Long Running Query Warning' AS [Configuration Parameter] , - N'300' , - N'Seconds' , - N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' +SET @insert_list += N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO ##BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, + PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, + ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, + LastExecutionTime, LastCompletionTime, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, + LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, + QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, + TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, + min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime, Pattern) ' ; - UNION ALL - SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; - RETURN; -END; +SET @body += N' +FROM (SELECT TOP (@Top) x.*, xpa.*, + CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY) as age_minutes, + CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY) as age_minutes_lifetime + FROM sys.#view# x + CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa + WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; -/*Validate version*/ -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @version_msg VARCHAR(8000); - SELECT @version_msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @version_msg; - RETURN; -END; +IF @SortOrder = 'duplicate' /* Issue #3345 */ + BEGIN + SET @body += N' INNER JOIN #duplicate_query_filter AS dqf ON x.sql_handle = dqf.sql_handle AND x.plan_handle = dqf.plan_handle AND x.creation_time = dqf.duplicate_creation_time ' + @nl ; + END -/* Set @Top based on sort */ -IF ( - @Top IS NULL - AND LOWER(@SortOrder) IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 5; - END; +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(x.plan_handle) AS deqps ' + @nl ; + END -IF ( - @Top IS NULL - AND LOWER(@SortOrder) NOT IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 10; - END; +SET @body += N' WHERE 1 = 1 ' + @nl ; -/* validate user inputs */ -IF @Top IS NULL - OR @SortOrder IS NULL - OR @QueryFilter IS NULL - OR @Reanalyze IS NULL -BEGIN - RAISERROR(N'Several parameters (@Top, @SortOrder, @QueryFilter, @renalyze) are required. Do not set them to NULL. Please try again.', 16, 1) WITH NOWAIT; - RETURN; -END; + 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 ; + END -RAISERROR(N'Checking @MinutesBack validity.', 0, 1) WITH NOWAIT; -IF @MinutesBack IS NOT NULL +IF @IgnoreSystemDBs = 1 BEGIN - IF @MinutesBack > 0 - BEGIN - RAISERROR(N'Setting @MinutesBack to a negative number', 0, 1) WITH NOWAIT; - SET @MinutesBack *=-1; - END; - IF @MinutesBack = 0 - BEGIN - RAISERROR(N'@MinutesBack can''t be 0, setting to -1', 0, 1) WITH NOWAIT; - SET @MinutesBack = -1; - END; - END; + RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; + SET @body += N' AND COALESCE(LOWER(DB_NAME(CAST(xpa.value AS INT))), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + END; +IF @DatabaseName IS NOT NULL OR @DatabaseName <> N'' + BEGIN + RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; + SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(N' + + QUOTENAME(@DatabaseName, N'''') + + N') ' + @nl; + END; -RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; +IF (SELECT COUNT(*) FROM #only_sql_handles) > 0 +BEGIN + RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; + SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; +END; -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults') IS NULL +IF (SELECT COUNT(*) FROM #ignore_sql_handles) > 0 BEGIN - CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) - ); + RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; + SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; +END; + +IF (SELECT COUNT(*) FROM #only_query_hashes) > 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #only_sql_handles) = 0 + AND (SELECT COUNT(*) FROM #ignore_sql_handles) = 0 +BEGIN + RAISERROR(N'Including only chosen Query Hashes', 0, 1) WITH NOWAIT; + SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_query_hashes q WHERE q.query_hash = x.query_hash) ' + @nl ; END; -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs') IS NULL +/* filtering for query hashes */ +IF (SELECT COUNT(*) FROM #ignore_query_hashes) > 0 + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 BEGIN - CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); + RAISERROR(N'Excluding chosen Query Hashes', 0, 1) WITH NOWAIT; + SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_query_hashes iq WHERE iq.query_hash = x.query_hash) ' + @nl ; END; +/* end filtering for query hashes */ -DECLARE @DurationFilter_i INT, - @MinMemoryPerQuery INT, - @msg NVARCHAR(4000) ; - - -IF @BringThePain = 1 - BEGIN - RAISERROR(N'You have chosen to bring the pain. Setting top to 2147483647.', 0, 1) WITH NOWAIT; - SET @Top = 2147483647; - END; - -/* Change duration from seconds to milliseconds */ IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; - SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); - END; + BEGIN + RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; + SET @body += N' AND (total_elapsed_time / 1000.0) / execution_count > @min_duration ' + @nl ; + END; -RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; -SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; -IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> '' -BEGIN - RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); - RETURN; -END; -IF (SELECT DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' -BEGIN - RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); - RETURN; -END; +IF @MinutesBack IS NOT NULL + BEGIN + RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; + SET @body += N' AND DATEADD(SECOND, (x.last_elapsed_time / 1000000.), x.last_execution_time) >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; + END; -SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; +IF @SlowlySearchPlansFor IS NOT NULL + BEGIN + RAISERROR(N'Setting string search for @SlowlySearchPlansFor, so remember, this is gonna be slow', 0, 1) WITH NOWAIT; + SET @SlowlySearchPlansFor = REPLACE((REPLACE((REPLACE((REPLACE(@SlowlySearchPlansFor, N'[', N'_')), N']', N'_')), N'^', N'_')), N'''', N''''''); + SET @body_where += N' AND CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE N''%' + @SlowlySearchPlansFor + N'%'' ' + @nl; + END -SET @SortOrder = LOWER(@SortOrder); -SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); -SET @SortOrder = REPLACE(@SortOrder, 'executions per minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'executions / minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'xpm', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'recent compilations', 'compiles'); -RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; -IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', - 'duration', 'avg duration', 'executions', 'avg executions', - 'compiles', 'memory grant', 'avg memory grant', - 'all', 'all avg') - BEGIN - RAISERROR(N'Invalid sort order chosen, reverting to cpu', 0, 1) WITH NOWAIT; - SET @SortOrder = 'cpu'; - END; +/* Apply the sort order here to only grab relevant plans. + This should make it faster to process since we'll be pulling back fewer + plans for processing. + */ +RAISERROR(N'Applying chosen sort order', 0, 1) WITH NOWAIT; +SELECT @body += N' ORDER BY ' + + CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' + WHEN N'reads' THEN N'total_logical_reads' + WHEN N'writes' THEN N'total_logical_writes' + WHEN N'duration' THEN N'total_elapsed_time' + WHEN N'executions' THEN N'execution_count' + WHEN N'compiles' THEN N'cached_time' + WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'total_worker_time / execution_count' + WHEN N'avg reads' THEN N'total_logical_reads / execution_count' + WHEN N'avg writes' THEN N'total_logical_writes / execution_count' + WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' + WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY))) AS money) + END ' + END + N' DESC ' + @nl ; -SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); -SET @QueryFilter = LOWER(@QueryFilter); + +SET @body += N') AS qs + CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, + SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, + SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, + SUM(CAST(total_logical_reads AS DECIMAL(30))) AS t_TotalReads, + SUM(CAST(total_logical_writes AS DECIMAL(30))) AS t_TotalWrites + FROM sys.#view#) AS t + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; -IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') - BEGIN - RAISERROR(N'Invalid query filter chosen. Reverting to all.', 0, 1) WITH NOWAIT; - SET @QueryFilter = 'all'; - END; +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(qs.plan_handle) AS deqps ' + @nl ; + END -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skip Analysis set to 1, hiding Summary', 0, 1) WITH NOWAIT; - SET @HideSummary = 1; - END; +SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; -IF @Reanalyze = 1 AND OBJECT_ID('tempdb..##bou_BlitzCacheResults') IS NULL - BEGIN - RAISERROR(N'##bou_BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; - SET @Reanalyze = 0; - END; +IF @NoobSaibot = 1 +BEGIN + SET @body_where += N' AND qp.query_plan.exist(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";//p:StmtSimple//p:MissingIndex'') = 1' + @nl ; +END -IF @Reanalyze = 0 - BEGIN - RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheResults - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - END; +SET @plans_triggers_select_list += N' +SELECT TOP (@Top) + @@SPID , + ''Procedure or Function: '' + + QUOTENAME(COALESCE(OBJECT_SCHEMA_NAME(qs.object_id, qs.database_id),'''')) + + ''.'' + + QUOTENAME(COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''')) AS QueryType, + COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), N''-- N/A --'') AS DatabaseName, + (total_worker_time / 1000.0) / execution_count AS AvgCPU , + (total_worker_time / 1000.0) AS TotalCPU , + CASE WHEN total_worker_time = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time)) AS MONEY) + END AS AverageCPUPerMinute , + CASE WHEN t.t_TotalWorker = 0 THEN 0 + ELSE CAST(ROUND(100.00 * (total_worker_time / 1000.0) / t.t_TotalWorker, 2) AS MONEY) + END AS PercentCPUByType, + CASE WHEN t.t_TotalElapsed = 0 THEN 0 + ELSE CAST(ROUND(100.00 * (total_elapsed_time / 1000.0) / t.t_TotalElapsed, 2) AS MONEY) + END AS PercentDurationByType, + CASE WHEN t.t_TotalReads = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) + END AS PercentReadsByType, + CASE WHEN t.t_TotalExecs = 0 THEN 0 + ELSE CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) + END AS PercentExecutionsByType, + (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , + (total_elapsed_time / 1000.0) AS TotalDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + execution_count AS ExecutionCount , + CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time))) AS money) + END AS ExecutionsPerMinute , + total_logical_writes AS TotalWrites , + total_logical_writes / execution_count AS AverageWrites , + CASE WHEN t.t_TotalWrites = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) + END AS PercentWritesByType, + CASE WHEN total_logical_writes = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0)) AS money) + END AS WritesPerMinute, + qs.cached_time AS PlanCreationTime, + qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, + NULL AS StatementStartOffset, + NULL AS StatementEndOffset, + NULL AS PlanGenerationNum, + NULL AS MinReturnedRows, + NULL AS MaxReturnedRows, + NULL AS AvgReturnedRows, + NULL AS TotalReturnedRows, + NULL AS LastReturnedRows, + NULL AS MinGrantKB, + NULL AS MaxGrantKB, + NULL AS MinUsedGrantKB, + NULL AS MaxUsedGrantKB, + NULL AS PercentMemoryGrantUsed, + NULL AS AvgMaxMemoryGrant,'; -IF @Reanalyze = 1 - BEGIN - RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; - GOTO Results; - END; + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, '; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; + + SET @plans_triggers_select_list += + N'st.text AS QueryText ,'; + + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @plans_triggers_select_list += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END; + ELSE + BEGIN + SET @plans_triggers_select_list += N' qp.query_plan AS QueryPlan, ' + @nl ; + END; -IF @SortOrder IN ('all', 'all avg') - BEGIN - RAISERROR(N'Checking all sort orders, please be patient', 0, 1) WITH NOWAIT; - GOTO AllSorts; - END; + SET @plans_triggers_select_list += + N't.t_TotalWorker, + t.t_TotalElapsed, + t.t_TotalReads, + t.t_TotalExecs, + t.t_TotalWrites, + qs.sql_handle AS SqlHandle, + qs.plan_handle AS PlanHandle, + NULL AS QueryHash, + NULL AS QueryPlanHash, + qs.min_worker_time / 1000.0, + qs.max_worker_time / 1000.0, + CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, + qs.min_elapsed_time / 1000.0, + qs.max_elapsed_time / 1000.0, + age_minutes, + age_minutes_lifetime, + @SortOrder '; -RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; -IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL - DROP TABLE #only_query_hashes ; +IF LEFT(@QueryFilter, 3) IN ('all', 'sta') +BEGIN + SET @sql += @insert_list; + + SET @sql += N' + SELECT TOP (@Top) + @@SPID , + ''Statement'' AS QueryType, + COALESCE(DB_NAME(CAST(pa.value AS INT)), N''-- N/A --'') AS DatabaseName, + (total_worker_time / 1000.0) / execution_count AS AvgCPU , + (total_worker_time / 1000.0) AS TotalCPU , + CASE WHEN total_worker_time = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time)) AS MONEY) + END AS AverageCPUPerMinute , + CASE WHEN t.t_TotalWorker = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_worker_time / t.t_TotalWorker, 2) AS MONEY) + END AS PercentCPUByType, + CASE WHEN t.t_TotalElapsed = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_elapsed_time / t.t_TotalElapsed, 2) AS MONEY) + END AS PercentDurationByType, + CASE WHEN t.t_TotalReads = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) + END AS PercentReadsByType, + CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) AS PercentExecutionsByType, + (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , + (total_elapsed_time / 1000.0) AS TotalDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + execution_count AS ExecutionCount , + CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time))) AS money) + END AS ExecutionsPerMinute , + total_logical_writes AS TotalWrites , + total_logical_writes / execution_count AS AverageWrites , + CASE WHEN t.t_TotalWrites = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) + END AS PercentWritesByType, + CASE WHEN total_logical_writes = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0)) AS money) + END AS WritesPerMinute, + qs.creation_time AS PlanCreationTime, + qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, + qs.statement_start_offset AS StatementStartOffset, + qs.statement_end_offset AS StatementEndOffset, + qs.plan_generation_num AS PlanGenerationNum, '; + + IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) + BEGIN + RAISERROR(N'Adding additional info columns for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + qs.min_rows AS MinReturnedRows, + qs.max_rows AS MaxReturnedRows, + CAST(qs.total_rows as MONEY) / execution_count AS AvgReturnedRows, + qs.total_rows AS TotalReturnedRows, + qs.last_rows AS LastReturnedRows, ' ; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for more info columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinReturnedRows, + NULL AS MaxReturnedRows, + NULL AS AvgReturnedRows, + NULL AS TotalReturnedRows, + NULL AS LastReturnedRows, ' ; + END; -IF OBJECT_ID('tempdb..#ignore_query_hashes') IS NOT NULL - DROP TABLE #ignore_query_hashes ; + IF @VersionShowsMemoryGrants = 1 + BEGIN + RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + min_grant_kb AS MinGrantKB, + max_grant_kb AS MaxGrantKB, + min_used_grant_kb AS MinUsedGrantKB, + max_used_grant_kb AS MaxUsedGrantKB, + CAST(ISNULL(NULLIF(( total_used_grant_kb * 1.00 ), 0) / NULLIF(total_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, + CAST(ISNULL(NULLIF(( total_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for memory grant columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinGrantKB, + NULL AS MaxGrantKB, + NULL AS MinUsedGrantKB, + NULL AS MaxUsedGrantKB, + NULL AS PercentMemoryGrantUsed, + NULL AS AvgMaxMemoryGrant, ' ; + END; -IF OBJECT_ID('tempdb..#only_sql_handles') IS NOT NULL - DROP TABLE #only_sql_handles ; + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills,'; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; + + SET @sql += N' + SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , ' + @nl ; -IF OBJECT_ID('tempdb..#ignore_sql_handles') IS NOT NULL - DROP TABLE #ignore_sql_handles ; - -IF OBJECT_ID('tempdb..#p') IS NOT NULL - DROP TABLE #p; -IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL - DROP TABLE #checkversion; + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @sql += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END + ELSE + BEGIN + SET @sql += N' query_plan AS QueryPlan, ' + @nl ; + END -IF OBJECT_ID ('tempdb..#configuration') IS NOT NULL - DROP TABLE #configuration; + SET @sql += N' + t.t_TotalWorker, + t.t_TotalElapsed, + t.t_TotalReads, + t.t_TotalExecs, + t.t_TotalWrites, + qs.sql_handle AS SqlHandle, + qs.plan_handle AS PlanHandle, + qs.query_hash AS QueryHash, + qs.query_plan_hash AS QueryPlanHash, + qs.min_worker_time / 1000.0, + qs.max_worker_time / 1000.0, + CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, + qs.min_elapsed_time / 1000.0, + qs.max_worker_time / 1000.0, + age_minutes, + age_minutes_lifetime, + @SortOrder '; + + SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; -IF OBJECT_ID ('tempdb..#stored_proc_info') IS NOT NULL - DROP TABLE #stored_proc_info; + SET @sort_filter += CASE @SortOrder WHEN N'cpu' THEN N'AND total_worker_time > 0' + WHEN N'reads' THEN N'AND total_logical_reads > 0' + WHEN N'writes' THEN N'AND total_logical_writes > 0' + WHEN N'duration' THEN N'AND total_elapsed_time > 0' + WHEN N'executions' THEN N'AND execution_count > 0' + /* WHEN N'compiles' THEN N'AND (age_minutes + age_minutes_lifetime) > 0' BGO 2021-01-24 commenting out for https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2772 */ + WHEN N'memory grant' THEN N'AND max_grant_kb > 0' + WHEN N'unused grant' THEN N'AND max_grant_kb > 0' + WHEN N'spills' THEN N'AND max_spills > 0' + /* And now the averages */ + WHEN N'avg cpu' THEN N'AND (total_worker_time / execution_count) > 0' + WHEN N'avg reads' THEN N'AND (total_logical_reads / execution_count) > 0' + WHEN N'avg writes' THEN N'AND (total_logical_writes / execution_count) > 0' + WHEN N'avg duration' THEN N'AND (total_elapsed_time / execution_count) > 0' + WHEN N'avg memory grant' THEN N'AND CASE WHEN max_grant_kb = 0 THEN 0 ELSE (max_grant_kb / execution_count) END > 0' + WHEN N'avg spills' THEN N'AND CASE WHEN total_spills = 0 THEN 0 ELSE (total_spills / execution_count) END > 0' + WHEN N'avg executions' THEN N'AND CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END > 0' + ELSE N' /* No minimum threshold set */ ' + END; -IF OBJECT_ID ('tempdb..#plan_creation') IS NOT NULL - DROP TABLE #plan_creation; + SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; -IF OBJECT_ID ('tempdb..#est_rows') IS NOT NULL - DROP TABLE #est_rows; + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl; -IF OBJECT_ID ('tempdb..#plan_cost') IS NOT NULL - DROP TABLE #plan_cost; + IF @SortOrder = 'compiles' + BEGIN + RAISERROR(N'Sorting by compiles', 0, 1) WITH NOWAIT; + SET @sql = REPLACE(@sql, '#sortable#', 'creation_time'); + END; +END; -IF OBJECT_ID ('tempdb..#proc_costs') IS NOT NULL - DROP TABLE #proc_costs; -IF OBJECT_ID ('tempdb..#stats_agg') IS NOT NULL - DROP TABLE #stats_agg; +IF (@QueryFilter = 'all' + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ + OR (LEFT(@QueryFilter, 3) = 'pro') +BEGIN + SET @sql += @insert_list; + SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Stored Procedure') ; -IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL - DROP TABLE #trace_flags; + SET @sql += REPLACE(@body, '#view#', 'dm_exec_procedure_stats') ; + SET @sql += @body_where ; -IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL - DROP TABLE #variable_info + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; -IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL - DROP TABLE #conversion_info + SET @sql += @sort_filter + @nl; + SET @sql += @body_order + @nl + @nl + @nl ; +END; -IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL - DROP TABLE #missing_index_xml +IF (@v >= 13 + AND @QueryFilter = 'all' + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ + AND (@SortOrder NOT IN ('spills', 'avg spills')) + OR (LEFT(@QueryFilter, 3) = 'fun') +BEGIN + SET @sql += @insert_list; + SET @sql += REPLACE(REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') + , N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, ', + N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ') ; -IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL - DROP TABLE #missing_index_schema + SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; + SET @sql += @body_where ; -IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL - DROP TABLE #missing_index_usage + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; -IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL - DROP TABLE #missing_index_detail + SET @sql += @sort_filter + @nl; -IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL - DROP TABLE #missing_index_pretty + SET @sql += @body_order + @nl + @nl + @nl ; +END; +/******************************************************************************* + * + * Because the trigger execution count in SQL Server 2008R2 and earlier is not + * correct, we ignore triggers for these versions of SQL Server. If you'd like + * to include trigger numbers, just know that the ExecutionCount, + * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for + * triggers on these versions of SQL Server. + * + * This is why we can't have nice things. + * + ******************************************************************************/ +IF (@UseTriggersAnyway = 1 OR @v >= 11) + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 + AND (@QueryFilter = 'all') + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ +BEGIN + RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; -CREATE TABLE #only_query_hashes ( - query_hash BINARY(8) -); + /* Trigger level information from the plan cache */ + SET @sql += @insert_list ; -CREATE TABLE #ignore_query_hashes ( - query_hash BINARY(8) -); + SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; -CREATE TABLE #only_sql_handles ( - sql_handle VARBINARY(64) -); + SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; -CREATE TABLE #ignore_sql_handles ( - sql_handle VARBINARY(64) -); + SET @sql += @body_where ; -CREATE TABLE #p ( - SqlHandle VARBINARY(64), - TotalCPU BIGINT, - TotalDuration BIGINT, - TotalReads BIGINT, - TotalWrites BIGINT, - ExecutionCount BIGINT -); + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; -CREATE TABLE #checkversion ( - version NVARCHAR(128), - common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), - major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), - minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), - build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), - revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) -); + SET @sql += @sort_filter + @nl; -CREATE TABLE #configuration ( - parameter_name VARCHAR(100), - value DECIMAL(38,0) -); + SET @sql += @body_order + @nl + @nl + @nl ; +END; -CREATE TABLE #stored_proc_info -( - SPID INT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - variable_name NVARCHAR(128), - variable_datatype NVARCHAR(128), - converted_column_name NVARCHAR(128), - compile_time_value NVARCHAR(128), - proc_name NVARCHAR(300), - column_name NVARCHAR(128), - converted_to NVARCHAR(128) -); -CREATE TABLE #plan_creation -( - percent_24 DECIMAL(5, 2), - percent_4 DECIMAL(5, 2), - percent_1 DECIMAL(5, 2), - total_plans INT, - SPID INT -); -CREATE TABLE #est_rows -( - QueryHash BINARY(8), - estimated_rows FLOAT -); +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' + WHEN N'reads' THEN N'total_logical_reads' + WHEN N'writes' THEN N'total_logical_writes' + WHEN N'duration' THEN N'total_elapsed_time' + WHEN N'executions' THEN N'execution_count' + WHEN N'compiles' THEN N'cached_time' + WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'total_worker_time / execution_count' + WHEN N'avg reads' THEN N'total_logical_reads / execution_count' + WHEN N'avg writes' THEN N'total_logical_writes / execution_count' + WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' + WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; -CREATE TABLE #plan_cost -( - QueryPlanCost FLOAT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - QueryPlanHash BINARY(8) -); +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); -CREATE TABLE #proc_costs -( - PlanTotalQuery FLOAT, - PlanHandle VARBINARY(64), - SqlHandle VARBINARY(64) -); +SET @sql += N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) +SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount +FROM (SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount, + ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn + FROM ##BlitzCacheProcs + WHERE SPID = @@SPID) AS x +WHERE x.rn = 1 +OPTION (RECOMPILE); -CREATE TABLE #stats_agg -( - SqlHandle VARBINARY(64), - LastUpdate DATETIME2(7), - ModificationCount INT, - SamplingPercent FLOAT, - [Statistics] NVARCHAR(256), - [Table] NVARCHAR(256), - [Schema] NVARCHAR(256), - [Database] NVARCHAR(256), -); +/* + This block was used to delete duplicate queries, but has been removed. + For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2026 +WITH d AS ( +SELECT SPID, + ROW_NUMBER() OVER (PARTITION BY SqlHandle, QueryHash ORDER BY #sortable# DESC) AS rn +FROM ##BlitzCacheProcs +WHERE SPID = @@SPID +) +DELETE d +WHERE d.rn > 1 +AND SPID = @@SPID +OPTION (RECOMPILE); +*/ +'; -CREATE TABLE #trace_flags -( - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - global_trace_flags VARCHAR(1000), - session_trace_flags VARCHAR(1000) -); +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' + WHEN N'reads' THEN N'TotalReads' + WHEN N'writes' THEN N'TotalWrites' + WHEN N'duration' THEN N'TotalDuration' + WHEN N'executions' THEN N'ExecutionCount' + WHEN N'compiles' THEN N'PlanCreationTime' + WHEN N'memory grant' THEN N'MaxGrantKB' + WHEN N'unused grant' THEN N'MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N'MaxSpills' + WHEN N'duplicate' THEN N'TotalCPU' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' + WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' + WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' + WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' + WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N'AvgSpills' + WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; -CREATE TABLE #variable_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - variable_name NVARCHAR(200), - variable_datatype NVARCHAR(128), - compile_time_value NVARCHAR(4000) -); +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); -CREATE TABLE #conversion_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - expression NVARCHAR(4000), - at_charindex AS CHARINDEX('@', expression), - bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), - comma_charindex AS CHARINDEX(',', expression) + 1, - second_comma_charindex AS - CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, - equal_charindex AS CHARINDEX('=', expression) + 1, - paren_charindex AS CHARINDEX('(', expression) + 1, - comma_paren_charindex AS - CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, - convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression) -); +IF @Debug = 1 + BEGIN + PRINT N'Printing dynamic SQL stored in @sql: '; + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; -CREATE TABLE #missing_index_xml -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - index_xml XML -); +RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; -CREATE TABLE #missing_index_schema -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - index_xml XML -); +IF OBJECT_ID('tempdb.dbo.##BlitzCacheResults') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheResults ( + SPID INT, + ID INT IDENTITY(1,1), + CheckID INT, + Priority TINYINT, + FindingsGroup VARCHAR(50), + Finding VARCHAR(500), + URL VARCHAR(200), + Details VARCHAR(4000) + ); +END; +ELSE +BEGIN + RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheResults + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END -CREATE TABLE #missing_index_usage -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - index_xml XML -); +IF OBJECT_ID('tempdb.dbo.##BlitzCacheProcs') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheProcs ( + SPID INT , + QueryType NVARCHAR(258), + DatabaseName sysname, + AverageCPU DECIMAL(38,4), + AverageCPUPerMinute DECIMAL(38,4), + TotalCPU DECIMAL(38,4), + PercentCPUByType MONEY, + PercentCPU MONEY, + AverageDuration DECIMAL(38,4), + TotalDuration DECIMAL(38,4), + PercentDuration MONEY, + PercentDurationByType MONEY, + AverageReads BIGINT, + TotalReads BIGINT, + PercentReads MONEY, + PercentReadsByType MONEY, + ExecutionCount BIGINT, + PercentExecutions MONEY, + PercentExecutionsByType MONEY, + ExecutionsPerMinute MONEY, + TotalWrites BIGINT, + AverageWrites MONEY, + PercentWrites MONEY, + PercentWritesByType MONEY, + WritesPerMinute MONEY, + PlanCreationTime DATETIME, + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' + ELSE 'N/A' END, + SqlHandle VARBINARY(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' + ELSE 'N/A' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' + ELSE 'N/A' END, + QueryHash BINARY(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' + ELSE 'N/A' END, + QueryPlanHash BINARY(8), + StatementStartOffset INT, + StatementEndOffset INT, + PlanGenerationNum BIGINT, + MinReturnedRows BIGINT, + MaxReturnedRows BIGINT, + AverageReturnedRows MONEY, + TotalReturnedRows BIGINT, + LastReturnedRows BIGINT, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryText NVARCHAR(MAX), + QueryPlan XML, + /* these next four columns are the total for the type of query. + don't actually use them for anything apart from math by type. + */ + TotalWorkerTimeForType BIGINT, + TotalElapsedTimeForType BIGINT, + TotalReadsForType BIGINT, + TotalExecutionCountForType BIGINT, + TotalWritesForType BIGINT, + NumberOfPlans INT, + NumberOfDistinctPlans INT, + SerialDesiredMemory FLOAT, + SerialRequiredMemory FLOAT, + CachedPlanSize FLOAT, + CompileTime FLOAT, + CompileCPU FLOAT , + CompileMemory FLOAT , + MaxCompileMemory FLOAT , + min_worker_time BIGINT, + max_worker_time BIGINT, + is_forced_plan BIT, + is_forced_parameterized BIT, + is_cursor BIT, + is_optimistic_cursor BIT, + is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, + is_parallel BIT, + is_forced_serial BIT, + is_key_lookup_expensive BIT, + key_lookup_cost FLOAT, + is_remote_query_expensive BIT, + remote_query_cost FLOAT, + frequent_execution BIT, + parameter_sniffing BIT, + unparameterized_query BIT, + near_parallel BIT, + plan_warnings BIT, + plan_multiple_plans INT, + long_running BIT, + downlevel_estimator BIT, + implicit_conversions BIT, + busy_loops BIT, + tvf_join BIT, + tvf_estimate BIT, + compile_timeout BIT, + compile_memory_limit_exceeded BIT, + warning_no_join_predicate BIT, + QueryPlanCost FLOAT, + missing_index_count INT, + unmatched_index_count INT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + age_minutes MONEY, + age_minutes_lifetime MONEY, + is_trivial BIT, + trace_flags_session VARCHAR(1000), + is_unused_grant BIT, + function_count INT, + clr_function_count INT, + is_table_variable BIT, + no_stats_warning BIT, + relop_warnings BIT, + is_table_scan BIT, + backwards_scan BIT, + forced_index BIT, + forced_seek BIT, + forced_scan BIT, + columnstore_row_mode BIT, + is_computed_scalar BIT , + is_sort_expensive BIT, + sort_cost FLOAT, + is_computed_filter BIT, + op_name VARCHAR(100) NULL, + index_insert_count INT NULL, + index_update_count INT NULL, + index_delete_count INT NULL, + cx_insert_count INT NULL, + cx_update_count INT NULL, + cx_delete_count INT NULL, + table_insert_count INT NULL, + table_update_count INT NULL, + table_delete_count INT NULL, + index_ops AS (index_insert_count + index_update_count + index_delete_count + + cx_insert_count + cx_update_count + cx_delete_count + + table_insert_count + table_update_count + table_delete_count), + is_row_level BIT, + is_spatial BIT, + index_dml BIT, + table_dml BIT, + long_running_low_cpu BIT, + low_cost_high_cpu BIT, + stale_stats BIT, + is_adaptive BIT, + index_spool_cost FLOAT, + index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, + is_spool_expensive BIT, + is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, + estimated_rows FLOAT, + is_bad_estimate BIT, + is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, + implicit_conversion_info XML, + cached_execution_parameters XML, + missing_indexes XML, + SetOptions VARCHAR(MAX), + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) + ); +END; +ELSE +BEGIN + RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheProcs + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END +IF @Reanalyze = 0 +BEGIN + RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; -CREATE TABLE #missing_index_detail -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - column_name NVARCHAR(128) -); + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @SortOrder NVARCHAR(20)', @Top, @DurationFilter_i, @MinutesBack, @SortOrder; +END; +IF @SkipAnalysis = 1 + BEGIN + RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; + GOTO Results ; + END; -CREATE TABLE #missing_index_pretty -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), - details AS N'/* ' - + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' - + CONVERT(NVARCHAR(30), impact) - + '%.' - + CHAR(10) - + N'*/' - + CHAR(10) + CHAR(13) - + N'/* ' - + CHAR(10) - + N'USE ' - + database_name - + CHAR(10) - + N'GO' - + CHAR(10) + CHAR(13) - + N'CREATE NONCLUSTERED INDEX ix_' - + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') - + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END - + CHAR(10) - + N' ON ' - + schema_name - + N'.' - + table_name - + N' (' + - + CASE WHEN equality IS NOT NULL - THEN equality - + CASE WHEN inequality IS NOT NULL - THEN N', ' + inequality - ELSE N'' - END - ELSE inequality - END - + N')' - + CHAR(10) - + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' - END - + CHAR(10) - + N'GO' - + CHAR(10) - + N'*/' -); -RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; -WITH x AS ( -SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 4 THEN 1 ELSE 0 END) AS [plans_4], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 1 THEN 1 ELSE 0 END) AS [plans_1], - COUNT(deqs.creation_time) AS [total_plans] -FROM sys.dm_exec_query_stats AS deqs -) -INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) -SELECT CONVERT(DECIMAL(3,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], - x.total_plans, - @@SPID AS SPID -FROM x +/* Update ##BlitzCacheProcs to get Stored Proc info + * This should get totals for all statements in a Stored Proc + */ +RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; +;WITH agg AS ( + SELECT + b.SqlHandle, + SUM(b.MinReturnedRows) AS MinReturnedRows, + SUM(b.MaxReturnedRows) AS MaxReturnedRows, + SUM(b.AverageReturnedRows) AS AverageReturnedRows, + SUM(b.TotalReturnedRows) AS TotalReturnedRows, + SUM(b.LastReturnedRows) AS LastReturnedRows, + SUM(b.MinGrantKB) AS MinGrantKB, + SUM(b.MaxGrantKB) AS MaxGrantKB, + SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, + SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB, + SUM(b.MinSpills) AS MinSpills, + SUM(b.MaxSpills) AS MaxSpills, + SUM(b.TotalSpills) AS TotalSpills + FROM ##BlitzCacheProcs b + WHERE b.SPID = @@SPID + AND b.QueryHash IS NOT NULL + GROUP BY b.SqlHandle +) +UPDATE b + SET + b.MinReturnedRows = b2.MinReturnedRows, + b.MaxReturnedRows = b2.MaxReturnedRows, + b.AverageReturnedRows = b2.AverageReturnedRows, + b.TotalReturnedRows = b2.TotalReturnedRows, + b.LastReturnedRows = b2.LastReturnedRows, + b.MinGrantKB = b2.MinGrantKB, + b.MaxGrantKB = b2.MaxGrantKB, + b.MinUsedGrantKB = b2.MinUsedGrantKB, + b.MaxUsedGrantKB = b2.MaxUsedGrantKB, + b.MinSpills = b2.MinSpills, + b.MaxSpills = b2.MaxSpills, + b.TotalSpills = b2.TotalSpills +FROM ##BlitzCacheProcs b +JOIN agg b2 +ON b2.SqlHandle = b.SqlHandle +WHERE b.QueryHash IS NULL +AND b.SPID = @@SPID OPTION (RECOMPILE) ; +/* Compute the total CPU, etc across our active set of the plan cache. + * Yes, there's a flaw - this doesn't include anything outside of our @Top + * metric. + */ +RAISERROR('Computing CPU, duration, read, and write metrics', 0, 1) WITH NOWAIT; +DECLARE @total_duration BIGINT, + @total_cpu BIGINT, + @total_reads BIGINT, + @total_writes BIGINT, + @total_execution_count BIGINT; -SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; -SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; -SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; +SELECT @total_cpu = SUM(TotalCPU), + @total_duration = SUM(TotalDuration), + @total_reads = SUM(TotalReads), + @total_writes = SUM(TotalWrites), + @total_execution_count = SUM(ExecutionCount) +FROM #p +OPTION (RECOMPILE) ; -DECLARE @individual VARCHAR(100) ; +DECLARE @cr NVARCHAR(1) = NCHAR(13); +DECLARE @lf NVARCHAR(1) = NCHAR(10); +DECLARE @tab NVARCHAR(1) = NCHAR(9); -IF (@OnlySqlHandles IS NOT NULL AND @IgnoreSqlHandles IS NOT NULL) -BEGIN -RAISERROR('You shouldn''t need to ignore and filter on SqlHandle at the same time.', 0, 1) WITH NOWAIT; -RETURN; -END; +/* Update CPU percentage for stored procedures */ +RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET PercentCPU = y.PercentCPU, + PercentDuration = y.PercentDuration, + PercentReads = y.PercentReads, + PercentWrites = y.PercentWrites, + PercentExecutions = y.PercentExecutions, + ExecutionsPerMinute = y.ExecutionsPerMinute, + /* Strip newlines and tabs. Tabs are replaced with multiple spaces + so that the later whitespace trim will completely eliminate them + */ + 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 + ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, + CASE @total_duration WHEN 0 THEN 0 + ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, + CASE @total_reads WHEN 0 THEN 0 + ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, + CASE @total_writes WHEN 0 THEN 0 + ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, + CASE @total_execution_count WHEN 0 THEN 0 + ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, + CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) + WHEN 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) + END AS ExecutionsPerMinute + FROM ( + SELECT PlanHandle, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + FROM ##BlitzCacheProcs + WHERE PlanHandle IS NOT NULL + AND SPID = @@SPID + GROUP BY PlanHandle, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + ) AS x +) AS y +WHERE ##BlitzCacheProcs.PlanHandle = y.PlanHandle + AND ##BlitzCacheProcs.PlanHandle IS NOT NULL + AND ##BlitzCacheProcs.SPID = @@SPID +OPTION (RECOMPILE) ; -IF (@StoredProcName IS NOT NULL AND (@OnlySqlHandles IS NOT NULL OR @IgnoreSqlHandles IS NOT NULL)) -BEGIN -RAISERROR('You can''t filter on stored procedure name and SQL Handle.', 0, 1) WITH NOWAIT; -RETURN; -END; -IF @OnlySqlHandles IS NOT NULL - AND LEN(@OnlySqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles', 0, 1) WITH NOWAIT; - SET @individual = ''; +RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET PercentCPU = y.PercentCPU, + PercentDuration = y.PercentDuration, + PercentReads = y.PercentReads, + PercentWrites = y.PercentWrites, + PercentExecutions = y.PercentExecutions, + ExecutionsPerMinute = y.ExecutionsPerMinute, + /* Strip newlines and tabs. Tabs are replaced with multiple spaces + so that the later whitespace trim will completely eliminate them + */ + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END +FROM ( + SELECT DatabaseName, + SqlHandle, + QueryHash, + CASE @total_cpu WHEN 0 THEN 0 + ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, + CASE @total_duration WHEN 0 THEN 0 + ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, + CASE @total_reads WHEN 0 THEN 0 + ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, + CASE @total_writes WHEN 0 THEN 0 + ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, + CASE @total_execution_count WHEN 0 THEN 0 + ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, + CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) + WHEN 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) + END AS ExecutionsPerMinute + FROM ( + SELECT DatabaseName, + SqlHandle, + QueryHash, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + FROM ##BlitzCacheProcs + WHERE SPID = @@SPID + GROUP BY DatabaseName, + SqlHandle, + QueryHash, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + ) AS x +) AS y +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle + AND ##BlitzCacheProcs.QueryHash = y.QueryHash + AND ##BlitzCacheProcs.DatabaseName = y.DatabaseName + AND ##BlitzCacheProcs.PlanHandle IS NULL +OPTION (RECOMPILE) ; - WHILE LEN(@OnlySqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @OnlySqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlySqlHandles, 0, PATINDEX('%,%',@OnlySqlHandles)) ; - - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - SET @OnlySqlHandles = SUBSTRING(@OnlySqlHandles, LEN(@individual + ',') + 1, LEN(@OnlySqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @OnlySqlHandles; - SET @OnlySqlHandles = NULL; - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; +/* Testing using XML nodes to speed up processing */ +RAISERROR(N'Begin XML nodes processing', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + PlanHandle, + q.n.query('.') AS statement, + 0 AS is_cursor +INTO #statements +FROM ##BlitzCacheProcs p + CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) +WHERE p.SPID = @@SPID +OPTION (RECOMPILE) ; - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT #statements +SELECT QueryHash , + SqlHandle , + PlanHandle, + q.n.query('.') AS statement, + 1 AS is_cursor +FROM ##BlitzCacheProcs p + CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) +WHERE p.SPID = @@SPID +OPTION (RECOMPILE) ; -IF @IgnoreSqlHandles IS NOT NULL - AND LEN(@IgnoreSqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles To Ignore', 0, 1) WITH NOWAIT; - SET @individual = ''; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + q.n.query('.') AS query_plan +INTO #query_plan +FROM #statements p + CROSS APPLY p.statement.nodes('//p:QueryPlan') AS q(n) +OPTION (RECOMPILE) ; - WHILE LEN(@IgnoreSqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreSqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreSqlHandles, 0, PATINDEX('%,%',@IgnoreSqlHandles)) ; - - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + q.n.query('.') AS relop +INTO #relop +FROM #query_plan p + CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) +OPTION (RECOMPILE) ; - SET @IgnoreSqlHandles = SUBSTRING(@IgnoreSqlHandles, LEN(@individual + ',') + 1, LEN(@IgnoreSqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreSqlHandles; - SET @IgnoreSqlHandles = NULL; +-- high level plan stuff +RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET NumberOfDistinctPlans = distinct_plan_count, + NumberOfPlans = number_of_plans , + plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN number_of_plans END +FROM + ( + SELECT + DatabaseName = + DB_NAME(CONVERT(int, pa.value)), + QueryHash = + qs.query_hash, + number_of_plans = + COUNT_BIG(qs.query_plan_hash), + distinct_plan_count = + COUNT_BIG(DISTINCT qs.query_plan_hash) + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = 'dbid' + GROUP BY + DB_NAME(CONVERT(int, pa.value)), + qs.query_hash +) AS x +WHERE ##BlitzCacheProcs.QueryHash = x.QueryHash +AND ##BlitzCacheProcs.DatabaseName = x.DatabaseName +OPTION (RECOMPILE) ; - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; +-- query level checks +RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , + unmatched_index_count = CASE WHEN is_trivial <> 1 THEN query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') END , + SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , + SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), + CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , + CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , + CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , + CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float'), + MaxCompileMemory = query_plan.value('sum(//p:QueryPlan/p:OptimizerHardwareDependentProperties/@MaxCompileMemory)', 'float') +FROM #query_plan qp +WHERE qp.QueryHash = ##BlitzCacheProcs.QueryHash +AND qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; +-- statement level checks +RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET compile_timeout = 1 +FROM #statements s +JOIN ##BlitzCacheProcs b +ON s.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 +OPTION (RECOMPILE); -IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' +RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET compile_memory_limit_exceeded = 1 +FROM #statements s +JOIN ##BlitzCacheProcs b +ON s.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 +OPTION (RECOMPILE); +IF @ExpertMode > 0 BEGIN - RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; - INSERT #only_sql_handles - ( sql_handle ) - SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) - FROM sys.dm_exec_procedure_stats AS deps - WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName - OPTION (RECOMPILE) ; - - IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 - BEGIN - RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; - RETURN; - END; - +RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +unparameterized_query AS ( + SELECT s.QueryHash, + unparameterized_query = CASE WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 1 AND + statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList/p:ColumnReference') = 0 THEN 1 + WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 0 AND + statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/*/p:RelOp/descendant::p:ScalarOperator/p:Identifier/p:ColumnReference[contains(@Column, "@")]') = 1 THEN 1 + END + FROM #statements AS s + ) +UPDATE b +SET b.unparameterized_query = u.unparameterized_query +FROM ##BlitzCacheProcs b +JOIN unparameterized_query u +ON u.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE u.unparameterized_query = 1 +OPTION (RECOMPILE); END; - -IF ((@OnlyQueryHashes IS NOT NULL AND LEN(@OnlyQueryHashes) > 0) - OR (@IgnoreQueryHashes IS NOT NULL AND LEN(@IgnoreQueryHashes) > 0)) - AND LEFT(@QueryFilter, 3) IN ('pro', 'fun') +IF @ExpertMode > 0 BEGIN - RAISERROR('You cannot limit by query hash and filter by stored procedure', 16, 1); - RETURN; +RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +index_dml AS ( + SELECT s.QueryHash, + index_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 + WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 + END + FROM #statements s + ) + UPDATE b + SET b.index_dml = i.index_dml + FROM ##BlitzCacheProcs AS b + JOIN index_dml i + ON i.QueryHash = b.QueryHash + WHERE i.index_dml = 1 + AND b.SPID = @@SPID + OPTION (RECOMPILE); END; -/* If the user is attempting to limit by query hash, set up the - #only_query_hashes temp table. This will be used to narrow down - results. - Just a reminder: Using @OnlyQueryHashes will ignore stored - procedures and triggers. - */ -IF @OnlyQueryHashes IS NOT NULL - AND LEN(@OnlyQueryHashes) > 0 +IF @ExpertMode > 0 BEGIN - RAISERROR(N'Setting up filter for Query Hashes', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@OnlyQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @OnlyQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlyQueryHashes, 0, PATINDEX('%,%',@OnlyQueryHashes)) ; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @OnlyQueryHashes = SUBSTRING(@OnlyQueryHashes, LEN(@individual + ',') + 1, LEN(@OnlyQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @OnlyQueryHashes; - SET @OnlyQueryHashes = NULL; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; +RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +table_dml AS ( + SELECT s.QueryHash, + table_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 + WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 + END + FROM #statements AS s + ) + UPDATE b + SET b.table_dml = t.table_dml + FROM ##BlitzCacheProcs AS b + JOIN table_dml t + ON t.QueryHash = b.QueryHash + WHERE t.table_dml = 1 + AND b.SPID = @@SPID + OPTION (RECOMPILE); +END; -/* If the user is setting up a list of query hashes to ignore, those - values will be inserted into #ignore_query_hashes. This is used to - exclude values from query results. - Just a reminder: Using @IgnoreQueryHashes will ignore stored - procedures and triggers. - */ -IF @IgnoreQueryHashes IS NOT NULL - AND LEN(@IgnoreQueryHashes) > 0 +IF @ExpertMode > 0 BEGIN - RAISERROR(N'Setting up filter to ignore query hashes', 0, 1) WITH NOWAIT; - SET @individual = '' ; - - WHILE LEN(@IgnoreQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreQueryHashes, 0, PATINDEX('%,%',@IgnoreQueryHashes)) ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - SET @IgnoreQueryHashes = SUBSTRING(@IgnoreQueryHashes, LEN(@individual + ',') + 1, LEN(@IgnoreQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreQueryHashes ; - SET @IgnoreQueryHashes = NULL ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - END; - END; -END; +RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT INTO #est_rows +SELECT DISTINCT + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, + c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows +FROM #statements AS s +CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) +WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; -IF @ConfigurationDatabaseName IS NOT NULL -BEGIN - RAISERROR(N'Reading values from Configuration Database', 0, 1) WITH NOWAIT; - DECLARE @config_sql NVARCHAR(MAX) = N'INSERT INTO #configuration SELECT parameter_name, value FROM ' - + QUOTENAME(@ConfigurationDatabaseName) - + '.' + QUOTENAME(@ConfigurationSchemaName) - + '.' + QUOTENAME(@ConfigurationTableName) - + ' ; ' ; - EXEC(@config_sql); + UPDATE b + SET b.estimated_rows = er.estimated_rows + FROM ##BlitzCacheProcs AS b + JOIN #est_rows er + ON er.QueryHash = b.QueryHash + WHERE b.SPID = @@SPID + AND b.QueryType = 'Statement' + OPTION (RECOMPILE); END; -RAISERROR(N'Setting up variables', 0, 1) WITH NOWAIT; -DECLARE @sql NVARCHAR(MAX) = N'', - @insert_list NVARCHAR(MAX) = N'', - @plans_triggers_select_list NVARCHAR(MAX) = N'', - @body NVARCHAR(MAX) = N'', - @body_where NVARCHAR(MAX) = N'WHERE 1 = 1 ' + @nl, - @body_order NVARCHAR(MAX) = N'ORDER BY #sortable# DESC OPTION (RECOMPILE) ', - - @q NVARCHAR(1) = N'''', - @pv VARCHAR(20), - @pos TINYINT, - @v DECIMAL(6,2), - @build INT; - - -RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; - -INSERT INTO #checkversion (version) -SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) +RAISERROR(N'Gathering trivial plans', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +UPDATE b +SET b.is_trivial = 1 +FROM ##BlitzCacheProcs AS b +JOIN ( +SELECT s.SqlHandle +FROM #statements AS s +JOIN ( SELECT r.SqlHandle + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[contains(@LogicalOp, "Scan")]') = 1 ) AS r + ON r.SqlHandle = s.SqlHandle +WHERE s.statement.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 +) AS s +ON b.SqlHandle = s.SqlHandle OPTION (RECOMPILE); -SELECT @v = common_version , - @build = build -FROM #checkversion +--Gather costs +RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT INTO #plan_cost ( QueryPlanCost, SqlHandle, PlanHandle, QueryHash, QueryPlanHash ) +SELECT DISTINCT + statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, + s.SqlHandle, + s.PlanHandle, + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash +FROM #statements s +CROSS APPLY s.statement.nodes('/p:StmtSimple') AS q(n) +WHERE statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 OPTION (RECOMPILE); -IF (@SortOrder IN ('memory grant', 'avg memory grant')) -AND ((@v < 11) -OR (@v = 11 AND @build < 6020) -OR (@v = 12 AND @build < 5000) -OR (@v = 13 AND @build < 1601)) -BEGIN - RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); - RETURN; -END; +RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; +WITH pc AS ( + SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle + FROM #plan_cost AS pc + GROUP BY pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle +) + UPDATE b + SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) + FROM pc + JOIN ##BlitzCacheProcs b + ON b.SqlHandle = pc.SqlHandle + AND b.QueryHash = pc.QueryHash + WHERE b.QueryType NOT LIKE '%Procedure%' + OPTION (RECOMPILE); + +IF EXISTS ( +SELECT 1 +FROM ##BlitzCacheProcs AS b +WHERE b.QueryType LIKE 'Procedure%' +) -IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) BEGIN - RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); - RETURN; -END; -RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; +RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, QueryCost AS ( + SELECT + DISTINCT + statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') AS SubTreeCost, + s.PlanHandle, + s.SqlHandle + FROM #statements AS s + WHERE PlanHandle IS NOT NULL +) +, QueryCostUpdate AS ( + SELECT + SUM(qc.SubTreeCost) OVER (PARTITION BY SqlHandle, PlanHandle) PlanTotalQuery, + qc.PlanHandle, + qc.SqlHandle + FROM QueryCost qc +) +INSERT INTO #proc_costs +SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle +FROM QueryCostUpdate AS qcu +OPTION (RECOMPILE); -SET @insert_list += N' -INSERT INTO ##bou_BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, - PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, - ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, - LastExecutionTime, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, - LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, - QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, - TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, - min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime) ' ; -SET @body += N' -FROM (SELECT TOP (@Top) x.*, xpa.*, - CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY) as age_minutes, - CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY) as age_minutes_lifetime - FROM sys.#view# x - CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa - WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; +UPDATE b + SET b.QueryPlanCost = ca.PlanTotalQuery +FROM ##BlitzCacheProcs AS b +CROSS APPLY ( + SELECT TOP 1 PlanTotalQuery + FROM #proc_costs qcu + WHERE qcu.PlanHandle = b.PlanHandle + ORDER BY PlanTotalQuery DESC +) ca +WHERE b.QueryType LIKE 'Procedure%' +AND b.SPID = @@SPID +OPTION (RECOMPILE); -SET @body += N' WHERE 1 = 1 ' + @nl ; +END; +UPDATE b +SET b.QueryPlanCost = 0.0 +FROM ##BlitzCacheProcs b +WHERE b.QueryPlanCost IS NULL +AND b.SPID = @@SPID +OPTION (RECOMPILE); -IF @IgnoreSystemDBs = 1 - BEGIN - RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; - SET @body += N' AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - END; +RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET plan_warnings = 1 +FROM #query_plan qp +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 +OPTION (RECOMPILE); -IF @DatabaseName IS NOT NULL OR @DatabaseName <> '' - BEGIN - RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; - SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(' - + QUOTENAME(@DatabaseName, N'''') - + N') ' + @nl; - END; +RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET implicit_conversions = 1 +FROM #query_plan qp +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 +OPTION (RECOMPILE); -IF (SELECT COUNT(*) FROM #only_sql_handles) > 0 +-- operator level checks +IF @ExpertMode > 0 BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; +RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END +FROM ##BlitzCacheProcs p + JOIN ( + SELECT qs.SqlHandle, + relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , + relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions + FROM #relop qs + ) AS x ON p.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; -IF (SELECT COUNT(*) FROM #ignore_sql_handles) > 0 -BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; -IF (SELECT COUNT(*) FROM #only_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #only_sql_handles) = 0 - AND (SELECT COUNT(*) FROM #ignore_sql_handles) = 0 -BEGIN - RAISERROR(N'Including only chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_query_hashes q WHERE q.query_hash = x.query_hash) ' + @nl ; -END; +RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END +FROM ##BlitzCacheProcs p + JOIN ( + SELECT r.SqlHandle, + 1 AS tvf_join + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 + AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 + ) AS x ON p.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); -/* filtering for query hashes */ -IF (SELECT COUNT(*) FROM #ignore_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 +IF @ExpertMode > 0 BEGIN - RAISERROR(N'Excluding chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_query_hashes iq WHERE iq.query_hash = x.query_hash) ' + @nl ; -END; -/* end filtering for query hashes */ +RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT r.SqlHandle, + c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, + c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , + c.n.exist('//p:Warnings') AS relop_warnings +FROM #relop AS r +CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) +) +UPDATE p +SET p.warning_no_join_predicate = x.warning_no_join_predicate, + p.no_stats_warning = x.no_stats_warning, + p.relop_warnings = x.relop_warnings +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; - SET @body += N' AND (total_elapsed_time / 1000.0) / execution_count > @min_duration ' + @nl ; - END; +RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT r.SqlHandle, + c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char +FROM #relop r +CROSS APPLY r.relop.nodes('//p:Object') AS c(n) +) +UPDATE p +SET is_table_variable = 1 +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +WHERE x.first_char = '@' +OPTION (RECOMPILE); -IF @MinutesBack IS NOT NULL - BEGIN - RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; - SET @body += N' AND x.last_execution_time >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; - END; +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT qs.SqlHandle, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) +) +UPDATE p +SET p.function_count = x.function_count, + p.clr_function_count = x.clr_function_count +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; -/* Apply the sort order here to only grab relevant plans. - This should make it faster to process since we'll be pulling back fewer - plans for processing. - */ -RAISERROR(N'Applying chosen sort order', 0, 1) WITH NOWAIT; -SELECT @body += N' ORDER BY ' + - CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY))) AS money) - END ' - END + N' DESC ' + @nl ; +RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET key_lookup_cost = x.key_lookup_cost +FROM ( +SELECT + qs.SqlHandle, + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS key_lookup_cost +FROM #relop qs +WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 +GROUP BY qs.SqlHandle +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); - -SET @body += N') AS qs - CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, - SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, - SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, - SUM(CAST(total_logical_reads AS BIGINT)) AS t_TotalReads, - SUM(CAST(total_logical_writes AS BIGINT)) AS t_TotalWrites - FROM sys.#view#) AS t - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; -SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; +RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET remote_query_cost = x.remote_query_cost +FROM ( +SELECT + qs.SqlHandle, + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS remote_query_cost +FROM #relop qs +WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 +GROUP BY qs.SqlHandle +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET sort_cost = y.max_sort_cost +FROM ( + SELECT x.SqlHandle, MAX((x.sort_io + x.sort_cpu)) AS max_sort_cost + FROM ( + SELECT + qs.SqlHandle, + relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, + relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu + FROM #relop qs + WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 + ) AS x + GROUP BY x.SqlHandle + ) AS y +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +IF NOT EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN -SET @plans_triggers_select_list += N' -SELECT TOP (@Top) - @@SPID , - ''Procedure or Function: '' + COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''') AS QueryType, - COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_worker_time / 1000.0) / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_elapsed_time / 1000.0) / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CASE WHEN t.t_TotalExecs = 0 THEN 0 - ELSE CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) - END AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.cached_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - NULL AS StatementStartOffset, - NULL AS StatementEndOffset, - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, - st.text AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - NULL AS QueryHash, - NULL AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_elapsed_time / 1000.0, - age_minutes, - age_minutes_lifetime '; +RAISERROR(N'No cursor plans found, skipping', 0, 1) WITH NOWAIT; +END -IF LEFT(@QueryFilter, 3) IN ('all', 'sta') +IF EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) BEGIN - SET @sql += @insert_list; - - SET @sql += N' - SELECT TOP (@Top) - @@SPID , - ''Statement'' AS QueryType, - COALESCE(DB_NAME(CAST(pa.value AS INT)), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_worker_time / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_elapsed_time / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.creation_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - qs.statement_start_offset AS StatementStartOffset, - qs.statement_end_offset AS StatementEndOffset, '; - - IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) - BEGIN - RAISERROR(N'Adding additional info columns for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - qs.min_rows AS MinReturnedRows, - qs.max_rows AS MaxReturnedRows, - CAST(qs.total_rows as MONEY) / execution_count AS AvgReturnedRows, - qs.total_rows AS TotalReturnedRows, - qs.last_rows AS LastReturnedRows, ' ; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for more info columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, ' ; - END; - IF (@v = 11 AND @build >= 6020) OR (@v = 12 AND @build >= 5000) OR (@v = 13 AND @build >= 1601) - - BEGIN - RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - min_grant_kb AS MinGrantKB, - max_grant_kb AS MaxGrantKB, - min_used_grant_kb AS MinUsedGrantKB, - max_used_grant_kb AS MaxUsedGrantKB, - CAST(ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, - CAST(ISNULL(NULLIF(( max_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for memory grant columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, ' ; - END; +RAISERROR(N'Cursor plans found, investigating', 0, 1) WITH NOWAIT; - - SET @sql += N' - SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - qs.query_hash AS QueryHash, - qs.query_plan_hash AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_worker_time / 1000.0, - age_minutes, - age_minutes_lifetime '; - - SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; - - SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; - - SET @sql += @body_order + @nl + @nl + @nl; +RAISERROR(N'Checking for Optimistic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_optimistic_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); - IF @SortOrder = 'compiles' - BEGIN - RAISERROR(N'Sorting by compiles', 0, 1) WITH NOWAIT; - SET @sql = REPLACE(@sql, '#sortable#', 'creation_time'); - END; -END; +RAISERROR(N'Checking if cursor is Forward Only', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_forward_only_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); -IF (@QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'pro') -BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Stored Procedure') ; +RAISERROR(N'Checking if cursor is Fast Forward', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_fast_forward_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="FastForward"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); - SET @sql += REPLACE(@body, '#view#', 'dm_exec_procedure_stats') ; - SET @sql += @body_where ; - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; +RAISERROR(N'Checking for Dynamic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_cursor_dynamic = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="Dynamic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); - SET @sql += @body_order + @nl + @nl + @nl ; -END; +END -IF (@v >= 13 - AND @QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'fun') +IF @ExpertMode > 0 BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; +RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET +b.is_table_scan = x.is_table_scan, +b.backwards_scan = x.backwards_scan, +b.forced_index = x.forced_index, +b.forced_seek = x.forced_seek, +b.forced_scan = x.forced_scan +FROM ##BlitzCacheProcs b +JOIN ( +SELECT + qs.SqlHandle, + 0 AS is_table_scan, + q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, + q.n.value('@ForcedIndex', 'bit') AS forced_index, + q.n.value('@ForceSeek', 'bit') AS forced_seek, + q.n.value('@ForceScan', 'bit') AS forced_scan +FROM #relop qs +CROSS APPLY qs.relop.nodes('//p:IndexScan') AS q(n) +UNION ALL +SELECT + qs.SqlHandle, + 1 AS is_table_scan, + q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, + q.n.value('@ForcedIndex', 'bit') AS forced_index, + q.n.value('@ForceSeek', 'bit') AS forced_seek, + q.n.value('@ForceScan', 'bit') AS forced_scan +FROM #relop qs +CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) +) AS x ON b.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; -/******************************************************************************* - * - * Because the trigger execution count in SQL Server 2008R2 and earlier is not - * correct, we ignore triggers for these versions of SQL Server. If you'd like - * to include trigger numbers, just know that the ExecutionCount, - * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for - * triggers on these versions of SQL Server. - * - * This is why we can't have nice things. - * - ******************************************************************************/ -IF (@UseTriggersAnyway = 1 OR @v >= 11) - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (@QueryFilter = 'all') - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) +IF @ExpertMode > 0 BEGIN - RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; - - /* Trigger level information from the plan cache */ - SET @sql += @insert_list ; - - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; +RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_computed_scalar = x.computed_column_function +FROM ( +SELECT qs.SqlHandle, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) +WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; - SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; - SET @sql += @body_where ; +RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_computed_filter = x.filter_function +FROM ( +SELECT +r.SqlHandle, +c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function +FROM #relop AS r +CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) +) x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +IndexOps AS +( + SELECT + r.QueryHash, + c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, + c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, + c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, + c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, + c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, + c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, + c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, + c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, + c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, + c.n.exist('@PhysicalOp[.="Table Delete"]') AS td + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp') c(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) +), iops AS +( + SELECT ios.QueryHash, + SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, + SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, + SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, + SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, + SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, + SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, + SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, + SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, + SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count + FROM IndexOps AS ios + WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', + 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', + 'Table Insert', 'Table Delete', 'Table Update') + GROUP BY ios.QueryHash) +UPDATE b +SET b.index_insert_count = iops.index_insert_count, + b.index_update_count = iops.index_update_count, + b.index_delete_count = iops.index_delete_count, + b.cx_insert_count = iops.cx_insert_count, + b.cx_update_count = iops.cx_update_count, + b.cx_delete_count = iops.cx_delete_count, + b.table_insert_count = iops.table_insert_count, + b.table_update_count = iops.table_update_count, + b.table_delete_count = iops.table_delete_count +FROM ##BlitzCacheProcs AS b +JOIN iops ON iops.QueryHash = b.QueryHash +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; -DECLARE @sort NVARCHAR(MAX); -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_spatial = x.is_spatial +FROM ( +SELECT qs.SqlHandle, + 1 AS is_spatial +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) +WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); -SET @sql += N' -INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) -SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount -FROM (SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount, - ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn - FROM ##bou_BlitzCacheProcs) AS x -WHERE x.rn = 1 +RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT s.QueryHash + FROM #statements AS s + WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) +, spools +AS ( SELECT DISTINCT r.QueryHash, + c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, + c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, + c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds +FROM #relop AS r +JOIN selects AS s +ON s.QueryHash = r.QueryHash +CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) +WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 +) +UPDATE b + SET b.index_spool_rows = sp.estimated_rows, + b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b +JOIN spools sp +ON sp.QueryHash = b.QueryHash OPTION (RECOMPILE); -'; - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' - WHEN N'reads' THEN N'TotalReads' - WHEN N'writes' THEN N'TotalWrites' - WHEN N'duration' THEN N'TotalDuration' - WHEN N'executions' THEN N'ExecutionCount' - WHEN N'compiles' THEN N'PlanCreationTime' - WHEN N'memory grant' THEN N'MaxGrantKB' - WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' - WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' - WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' - WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' - WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' - WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); +RAISERROR('Checking for wonky Table Spools', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT s.QueryHash + FROM #statements AS s + WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) +, spools +AS ( SELECT DISTINCT r.QueryHash, + c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, + c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, + c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds +FROM #relop AS r +JOIN selects AS s +ON s.QueryHash = r.QueryHash +CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) +WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Table Spool" and @LogicalOp="Lazy Spool"]') = 1 +) +UPDATE b + SET b.table_spool_rows = (sp.estimated_rows * sp.estimated_rebinds), + b.table_spool_cost = ((sp.estimated_io * sp.estimated_cpu * sp.estimated_rows) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b +JOIN spools sp +ON sp.QueryHash = b.QueryHash +OPTION (RECOMPILE); -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; +RAISERROR('Checking for selects that cause non-spill and index spool writes', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT CONVERT(BINARY(8), + RIGHT('0000000000000000' + + SUBSTRING(s.statement.value('(/p:StmtSimple/@QueryHash)[1]', 'VARCHAR(18)'), + 3, 18), 16), 2) AS QueryHash + FROM #statements AS s + JOIN ##BlitzCacheProcs b + ON s.QueryHash = b.QueryHash + WHERE b.index_spool_rows IS NULL + AND b.index_spool_cost IS NULL + AND b.table_spool_cost IS NULL + AND b.table_spool_rows IS NULL + AND b.is_big_spills IS NULL + AND b.AverageWrites > 1024. + AND s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 +) +UPDATE b + SET b.select_with_writes = 1 +FROM ##BlitzCacheProcs b +JOIN selects AS s +ON s.QueryHash = b.QueryHash +AND b.AverageWrites > 1024.; -IF @Reanalyze = 0 +/* 2012+ only */ +IF @v >= 11 BEGIN - RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT', @Top, @DurationFilter_i, @MinutesBack; + RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE ##BlitzCacheProcs + SET is_forced_serial = 1 + FROM #query_plan qp + WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle + AND SPID = @@SPID + AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 + AND (##BlitzCacheProcs.is_parallel = 0 OR ##BlitzCacheProcs.is_parallel IS NULL) + OPTION (RECOMPILE); + + IF @ExpertMode > 0 + BEGIN + RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE ##BlitzCacheProcs + SET columnstore_row_mode = x.is_row_mode + FROM ( + SELECT + qs.SqlHandle, + relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode + FROM #relop qs + WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 + ) AS x + WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle + AND SPID = @@SPID + OPTION (RECOMPILE); + END; + END; +/* 2014+ only */ +IF @v >= 12 +BEGIN + RAISERROR('Checking for downlevel cardinality estimators being used on SQL Server 2014.', 0, 1) WITH NOWAIT; -/* Update ##bou_BlitzCacheProcs to get Stored Proc info - * This should get totals for all statements in a Stored Proc - */ -RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; -;WITH agg AS ( - SELECT - b.SqlHandle, - SUM(b.MinReturnedRows) AS MinReturnedRows, - SUM(b.MaxReturnedRows) AS MaxReturnedRows, - SUM(b.AverageReturnedRows) AS AverageReturnedRows, - SUM(b.TotalReturnedRows) AS TotalReturnedRows, - SUM(b.LastReturnedRows) AS LastReturnedRows, - SUM(b.MinGrantKB) AS MinGrantKB, - SUM(b.MaxGrantKB) AS MaxGrantKB, - SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, - SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB - FROM ##bou_BlitzCacheProcs b - WHERE b.SPID = @@SPID - AND b.QueryHash IS NOT NULL - GROUP BY b.SqlHandle + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE p + SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END + FROM ##BlitzCacheProcs p + JOIN #statements s ON p.QueryHash = s.QueryHash + WHERE SPID = @@SPID + OPTION (RECOMPILE); +END ; + +/* 2016+ only */ +IF @v >= 13 AND @ExpertMode > 0 +BEGIN + RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; + + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE p + SET p.is_row_level = 1 + FROM ##BlitzCacheProcs p + JOIN #statements s ON p.QueryHash = s.QueryHash + WHERE SPID = @@SPID + AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 + OPTION (RECOMPILE); +END ; + +/* 2017+ only */ +IF @v >= 14 OR (@v = 13 AND @build >= 5026) +BEGIN + +IF @ExpertMode > 0 +BEGIN +RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT INTO #stats_agg +SELECT qp.SqlHandle, + x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, + x.c.value('@ModificationCount', 'BIGINT') AS ModificationCount, + x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, + x.c.value('@Statistics', 'NVARCHAR(258)') AS [Statistics], + x.c.value('@Table', 'NVARCHAR(258)') AS [Table], + x.c.value('@Schema', 'NVARCHAR(258)') AS [Schema], + x.c.value('@Database', 'NVARCHAR(258)') AS [Database] +FROM #query_plan AS qp +CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) +OPTION (RECOMPILE); + + +RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; +WITH stale_stats AS ( + SELECT sa.SqlHandle + FROM #stats_agg AS sa + GROUP BY sa.SqlHandle + HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) + AND AVG(sa.ModificationCount) >= 100000 ) UPDATE b - SET - b.MinReturnedRows = b2.MinReturnedRows, - b.MaxReturnedRows = b2.MaxReturnedRows, - b.AverageReturnedRows = b2.AverageReturnedRows, - b.TotalReturnedRows = b2.TotalReturnedRows, - b.LastReturnedRows = b2.LastReturnedRows, - b.MinGrantKB = b2.MinGrantKB, - b.MaxGrantKB = b2.MaxGrantKB, - b.MinUsedGrantKB = b2.MinUsedGrantKB, - b.MaxUsedGrantKB = b2.MaxUsedGrantKB -FROM ##bou_BlitzCacheProcs b -JOIN agg b2 -ON b2.SqlHandle = b.SqlHandle -WHERE b.QueryHash IS NULL +SET stale_stats = 1 +FROM ##BlitzCacheProcs b +JOIN stale_stats os +ON b.SqlHandle = os.SqlHandle AND b.SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); +END; -/* Compute the total CPU, etc across our active set of the plan cache. - * Yes, there's a flaw - this doesn't include anything outside of our @Top - * metric. - */ -RAISERROR('Computing CPU, duration, read, and write metrics', 0, 1) WITH NOWAIT; -DECLARE @total_duration BIGINT, - @total_cpu BIGINT, - @total_reads BIGINT, - @total_writes BIGINT, - @total_execution_count BIGINT; +IF @v >= 14 AND @ExpertMode > 0 +BEGIN +RAISERROR('Checking for adaptive joins', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +aj AS ( + SELECT + SqlHandle + FROM #relop AS r + CROSS APPLY r.relop.nodes('//p:RelOp') x(c) + WHERE x.c.exist('@IsAdaptive[.=1]') = 1 +) +UPDATE b +SET b.is_adaptive = 1 +FROM ##BlitzCacheProcs b +JOIN aj +ON b.SqlHandle = aj.SqlHandle +AND b.SPID = @@SPID +OPTION (RECOMPILE); +END; -SELECT @total_cpu = SUM(TotalCPU), - @total_duration = SUM(TotalDuration), - @total_reads = SUM(TotalReads), - @total_writes = SUM(TotalWrites), - @total_execution_count = SUM(ExecutionCount) -FROM #p -OPTION (RECOMPILE) ; +IF ((@v >= 14 + OR (@v = 13 AND @build >= 5026) + OR (@v = 12 AND @build >= 6024)) + AND @ExpertMode > 0) -DECLARE @cr NVARCHAR(1) = NCHAR(13); -DECLARE @lf NVARCHAR(1) = NCHAR(10); -DECLARE @tab NVARCHAR(1) = NCHAR(9); +BEGIN; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +row_goals AS( +SELECT qs.QueryHash +FROM #relop qs +WHERE relop.value('sum(/p:RelOp/@EstimateRowsWithoutRowGoal)', 'float') > 0 +) +UPDATE b +SET b.is_row_goal = 1 +FROM ##BlitzCacheProcs b +JOIN row_goals +ON b.QueryHash = row_goals.QueryHash +AND b.SPID = @@SPID +OPTION (RECOMPILE); +END ; -/* Update CPU percentage for stored procedures */ -RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT PlanHandle, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE PlanHandle IS NOT NULL - AND SPID = @@SPID - GROUP BY PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.PlanHandle = y.PlanHandle - AND ##bou_BlitzCacheProcs.PlanHandle IS NOT NULL - AND ##bou_BlitzCacheProcs.SPID = @@SPID -OPTION (RECOMPILE) ; +END; -RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.SqlHandle = y.SqlHandle - AND ##bou_BlitzCacheProcs.QueryHash = y.QueryHash - AND ##bou_BlitzCacheProcs.DatabaseName = y.DatabaseName - AND ##bou_BlitzCacheProcs.PlanHandle IS NULL -OPTION (RECOMPILE) ; - +/* END Testing using XML nodes to speed up processing */ -/* Testing using XML nodes to speed up processing */ -RAISERROR(N'Begin XML nodes processing', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -INTO #statements -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; +/* Update to grab stored procedure name for individual statements */ +RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; +UPDATE p +SET QueryType = QueryType + ' (parent ' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + '.' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' +FROM ##BlitzCacheProcs p + JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle +WHERE QueryType = 'Statement' +AND SPID = @@SPID +OPTION (RECOMPILE); -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; +/* Update to grab stored procedure name for individual statements when PSPO is detected */ +UPDATE p +SET QueryType = QueryType + ' (parent ' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + '.' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' +FROM ##BlitzCacheProcs p + OUTER APPLY ( + SELECT REPLACE(REPLACE(REPLACE(REPLACE(p.QueryText, ' (', '('), '( ', '('), ' =', '='), '= ', '=') AS NormalizedQueryText + ) a + OUTER APPLY ( + SELECT CHARINDEX('option(PLAN PER VALUE(ObjectID=', a.NormalizedQueryText) AS OptionStart + ) b + OUTER APPLY ( + SELECT SUBSTRING(a.NormalizedQueryText, b.OptionStart + 31, LEN(a.NormalizedQueryText) - b.OptionStart - 30) AS OptionSubstring + WHERE b.OptionStart > 0 + ) c + OUTER APPLY ( + SELECT PATINDEX('%[^0-9]%', c.OptionSubstring) AS ObjectLength + ) d + OUTER APPLY ( + SELECT TRY_CAST(SUBSTRING(OptionSubstring, 1, d.ObjectLength - 1) AS INT) AS ObjectId + ) e + JOIN sys.dm_exec_procedure_stats s ON DB_ID(p.DatabaseName) = s.database_id AND e.ObjectId = s.object_id +WHERE p.QueryType = 'Statement' +AND p.SPID = @@SPID +AND s.object_id IS NOT NULL +OPTION (RECOMPILE); -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS query_plan -INTO #query_plan -FROM #statements p - CROSS APPLY p.statement.nodes('//p:QueryPlan') AS q(n) -OPTION (RECOMPILE) ; +RAISERROR(N'Attempting to get function name for individual statements', 0, 1) WITH NOWAIT; +DECLARE @function_update_sql NVARCHAR(MAX) = N'' +IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_update_sql = @function_update_sql + N' + UPDATE p + SET QueryType = QueryType + '' (parent '' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + ''.'' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + '')'' + FROM ##BlitzCacheProcs p + JOIN sys.dm_exec_function_stats s ON p.SqlHandle = s.sql_handle + WHERE QueryType = ''Statement'' + AND SPID = @@SPID + OPTION (RECOMPILE); + ' + EXEC sys.sp_executesql @function_update_sql + END + + +/* Trace Flag Checks 2012 SP3, 2014 SP2 and 2016 SP1 only)*/ +IF @v >= 11 +BEGIN -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS relop -INTO #relop -FROM #query_plan p - CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) -OPTION (RECOMPILE) ; +RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, tf_pretty AS ( +SELECT qp.QueryHash, + qp.SqlHandle, + q.n.value('@Value', 'INT') AS trace_flag, + q.n.value('@Scope', 'VARCHAR(10)') AS scope +FROM #query_plan qp +CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) +) +INSERT INTO #trace_flags +SELECT DISTINCT tf1.SqlHandle , tf1.QueryHash, + STUFF(( + SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) + FROM tf_pretty AS tf2 + WHERE tf1.SqlHandle = tf2.SqlHandle + AND tf1.QueryHash = tf2.QueryHash + AND tf2.scope = 'Global' + FOR XML PATH(N'')), 1, 2, N'' + ) AS global_trace_flags, + STUFF(( + SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) + FROM tf_pretty AS tf2 + WHERE tf1.SqlHandle = tf2.SqlHandle + AND tf1.QueryHash = tf2.QueryHash + AND tf2.scope = 'Session' + FOR XML PATH(N'')), 1, 2, N'' + ) AS session_trace_flags +FROM tf_pretty AS tf1 +OPTION (RECOMPILE); +UPDATE p +SET p.trace_flags_session = tf.session_trace_flags +FROM ##BlitzCacheProcs p +JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; --- high level plan stuff -RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans , - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END -FROM ( - SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; --- statement level checks -RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; +RAISERROR(N'Checking for MSTVFs', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b -SET compile_timeout = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 +SET b.is_mstvf = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp[(@EstimateRows="100" or @EstimateRows="1") and @LogicalOp="Table-valued function"]') = 1 OPTION (RECOMPILE); -RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for many to many merge joins', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b -SET compile_memory_limit_exceeded = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 +SET b.is_mm_join = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp/p:Merge/@ManyToMany[.="1"]') = 1 OPTION (RECOMPILE); +END ; -RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -unparameterized_query AS ( - SELECT s.QueryHash, - unparameterized_query = CASE WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 1 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList/p:ColumnReference') = 0 THEN 1 - WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 0 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/*/p:RelOp/descendant::p:ScalarOperator/p:Identifier/p:ColumnReference[contains(@Column, "@")]') = 1 THEN 1 - END - FROM #statements AS s - ) +is_paul_white_electric AS ( +SELECT 1 AS [is_paul_white_electric], +r.SqlHandle +FROM #relop AS r +CROSS APPLY r.relop.nodes('//p:RelOp') c(n) +WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 +) UPDATE b -SET b.unparameterized_query = u.unparameterized_query -FROM ##bou_BlitzCacheProcs b -JOIN unparameterized_query u -ON u.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE u.unparameterized_query = 1 +SET b.is_paul_white_electric = ipwe.is_paul_white_electric +FROM ##BlitzCacheProcs AS b +JOIN is_paul_white_electric ipwe +ON ipwe.SqlHandle = b.SqlHandle +WHERE b.SPID = @@SPID OPTION (RECOMPILE); +END ; -RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -index_dml AS ( - SELECT s.QueryHash, - index_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 - END - FROM #statements s - ) - UPDATE b - SET b.index_dml = i.index_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN index_dml i - ON i.QueryHash = b.QueryHash - WHERE i.index_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); - -RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -table_dml AS ( - SELECT s.QueryHash, - table_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 - END - FROM #statements AS s - ) - UPDATE b - SET b.table_dml = t.table_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN table_dml t - ON t.QueryHash = b.QueryHash - WHERE t.table_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); +RAISERROR(N'Checking for non-sargable predicates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, nsarg + AS ( SELECT r.QueryHash, 1 AS fn, 0 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator') AS ca(x) + WHERE ( ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName') = 1 + OR ca.x.exist('//p:ScalarOperator/p:IF') = 1 ) + UNION ALL + SELECT r.QueryHash, 0 AS fn, 1 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp//p:ScalarOperator') AS ca(x) + WHERE r.relop.exist('/p:RelOp[contains(@LogicalOp, "Join")]') = 1 + AND ca.x.exist('//p:ScalarOperator[contains(@ScalarString, "Expr")]') = 1 + UNION ALL + SELECT r.QueryHash, 0 AS fn, 0 AS jo, 1 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator') AS ca(x) + CROSS APPLY ca.x.nodes('//p:Const') AS co(x) + WHERE ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName[.="like"]') = 1 + AND ( ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') <> 'N' + AND co.x.value('substring(@ConstValue, 2, 1)', 'VARCHAR(100)') = '%' ) + OR ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') = 'N' + AND co.x.value('substring(@ConstValue, 3, 1)', 'VARCHAR(100)') = '%' ))), + d_nsarg + AS ( SELECT DISTINCT + nsarg.QueryHash + FROM nsarg + WHERE nsarg.fn = 1 + OR nsarg.jo = 1 + OR nsarg.lk = 1 ) +UPDATE b +SET b.is_nonsargable = 1 +FROM d_nsarg AS d +JOIN ##BlitzCacheProcs AS b + ON b.QueryHash = d.QueryHash +WHERE b.SPID = @@SPID +OPTION ( RECOMPILE ); +/*Begin implicit conversion and parameter info */ -RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT INTO #est_rows -SELECT DISTINCT - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows -FROM #statements AS s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) -WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; +RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; - UPDATE b - SET b.estimated_rows = er.estimated_rows - FROM ##bou_BlitzCacheProcs AS b - JOIN #est_rows er - ON er.QueryHash = b.QueryHash - WHERE b.SPID = @@SPID - AND b.QueryType = 'Statement' - OPTION (RECOMPILE); +RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT #variable_info ( SPID, QueryHash, SqlHandle, proc_name, variable_name, variable_datatype, compile_time_value ) +SELECT DISTINCT @@SPID, + qp.QueryHash, + qp.SqlHandle, + b.QueryType AS proc_name, + q.n.value('@Column', 'NVARCHAR(258)') AS variable_name, + q.n.value('@ParameterDataType', 'NVARCHAR(258)') AS variable_datatype, + q.n.value('@ParameterCompiledValue', 'NVARCHAR(258)') AS compile_time_value +FROM #query_plan AS qp +JOIN ##BlitzCacheProcs AS b +ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) +OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) +CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) +WHERE b.SPID = @@SPID +OPTION (RECOMPILE); ---Gather costs -RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #plan_cost -SELECT DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, - s.SqlHandle, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash -FROM #statements s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS q(n) -WHERE statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 +RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT #conversion_info ( SPID, QueryHash, SqlHandle, proc_name, expression ) +SELECT DISTINCT @@SPID, + qp.QueryHash, + qp.SqlHandle, + b.QueryType AS proc_name, + qq.c.value('@Expression', 'NVARCHAR(4000)') AS expression +FROM #query_plan AS qp +JOIN ##BlitzCacheProcs AS b +ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) +OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) +CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) +WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 + AND qp.QueryHash IS NOT NULL + AND b.implicit_conversions = 1 +AND b.SPID = @@SPID OPTION (RECOMPILE); -RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; -WITH pc AS ( - SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash - FROM #plan_cost AS pc - GROUP BY pc.QueryHash, pc.QueryPlanHash -) - UPDATE b - SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) - FROM pc - JOIN ##bou_BlitzCacheProcs b - ON b.QueryPlanHash = pc.QueryPlanHash - OR b.QueryHash = pc.QueryHash - WHERE b.QueryType NOT LIKE '%Procedure%' - OPTION (RECOMPILE); - -IF EXISTS ( -SELECT 1 -FROM ##bou_BlitzCacheProcs AS b -WHERE b.QueryType LIKE 'Procedure%' -) - -BEGIN - -RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, QueryCost AS ( - SELECT - DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') AS SubTreeCost, - s.PlanHandle, - s.SqlHandle - FROM #statements AS s - WHERE PlanHandle IS NOT NULL -) -, QueryCostUpdate AS ( - SELECT - SUM(qc.SubTreeCost) OVER (PARTITION BY SqlHandle, PlanHandle) PlanTotalQuery, - qc.PlanHandle, - qc.SqlHandle - FROM QueryCost qc -) -INSERT INTO #proc_costs -SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle -FROM QueryCostUpdate AS qcu; - -UPDATE b - SET b.QueryPlanCost = ca.PlanTotalQuery -FROM ##bou_BlitzCacheProcs AS b -CROSS APPLY ( - SELECT TOP 1 PlanTotalQuery - FROM #proc_costs qcu - WHERE qcu.PlanHandle = b.PlanHandle - ORDER BY PlanTotalQuery DESC -) ca -WHERE b.QueryType LIKE 'Procedure%' -AND b.SPID = @@SPID +RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; +INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) +SELECT @@SPID AS SPID, + ci.SqlHandle, + ci.QueryHash, + REPLACE(REPLACE(REPLACE(ci.proc_name, ')', ''), 'Statement (parent ', ''), 'Procedure or Function: ', '') AS proc_name, + CASE WHEN ci.at_charindex > 0 + AND ci.bracket_charindex > 0 + THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) + ELSE N'**no_variable**' + END AS variable_name, + N'**no_variable**' AS variable_datatype, + CASE WHEN ci.at_charindex = 0 + AND ci.comma_charindex > 0 + AND ci.second_comma_charindex > 0 + THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) + ELSE N'**no_column**' + END AS converted_column_name, + CASE WHEN ci.at_charindex = 0 + AND ci.equal_charindex > 0 + AND ci.convert_implicit_charindex = 0 + THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) + WHEN ci.at_charindex = 0 + AND (ci.equal_charindex -1) > 0 + AND ci.convert_implicit_charindex > 0 + THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) + WHEN ci.at_charindex > 0 + AND ci.comma_charindex > 0 + AND ci.second_comma_charindex > 0 + THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) + ELSE N'**no_column **' + END AS column_name, + CASE WHEN ci.paren_charindex > 0 + AND ci.comma_paren_charindex > 0 + THEN SUBSTRING(ci.expression, ci.paren_charindex, ci.comma_paren_charindex) + END AS converted_to, + 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, 258) AS compile_time_value +FROM #conversion_info AS ci OPTION (RECOMPILE); -END; - -UPDATE b -SET b.QueryPlanCost = 0.0 -FROM ##bou_BlitzCacheProcs b -WHERE b.QueryPlanCost IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); -RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET plan_warnings = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 +RAISERROR(N'Updating variables for inserted procs', 0, 1) WITH NOWAIT; +UPDATE sp +SET sp.variable_datatype = vi.variable_datatype, + sp.compile_time_value = vi.compile_time_value +FROM #stored_proc_info AS sp +JOIN #variable_info AS vi +ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) +OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) +AND sp.variable_name = vi.variable_name OPTION (RECOMPILE); -RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET implicit_conversions = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 -OPTION (RECOMPILE); --- operator level checks -RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , - relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions - FROM #relop qs - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID +RAISERROR(N'Inserting variables for other procs', 0, 1) WITH NOWAIT; +INSERT #stored_proc_info + ( SPID, SqlHandle, QueryHash, variable_name, variable_datatype, compile_time_value, proc_name ) +SELECT vi.SPID, vi.SqlHandle, vi.QueryHash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, REPLACE(REPLACE(REPLACE(vi.proc_name, ')', ''), 'Statement (parent ', ''), 'Procedure or Function: ', '') AS proc_name +FROM #variable_info AS vi +WHERE NOT EXISTS +( + SELECT * + FROM #stored_proc_info AS sp + WHERE (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) + OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) +) OPTION (RECOMPILE); -RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT r.SqlHandle, - 1 AS tvf_join - FROM #relop AS r - WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 - AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE); -RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, - c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , - c.n.exist('//p:Warnings') AS relop_warnings -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) -) -UPDATE p -SET p.warning_no_join_predicate = x.warning_no_join_predicate, - p.no_stats_warning = x.no_stats_warning, - p.relop_warnings = x.relop_warnings -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID +RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; +UPDATE s +SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' + THEN LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) + ELSE s.variable_datatype + END, + s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' + THEN LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) + ELSE s.converted_to + END, + s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' + THEN SUBSTRING(s.compile_time_value, + CHARINDEX('(', s.compile_time_value) + 1, + CHARINDEX(')', s.compile_time_value, CHARINDEX('(', s.compile_time_value) + 1) - 1 - CHARINDEX('(', s.compile_time_value) + ) + WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') + AND s.variable_datatype NOT LIKE '%binary%' + AND s.compile_time_value NOT LIKE 'N''%''' + AND s.compile_time_value NOT LIKE '''%''' + AND s.compile_time_value <> s.column_name + AND s.compile_time_value <> '**idk_man**' + THEN QUOTENAME(compile_time_value, '''') + ELSE s.compile_time_value + END +FROM #stored_proc_info AS s OPTION (RECOMPILE); -RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; +RAISERROR(N'Updating SET options', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char -FROM #relop r -CROSS APPLY r.relop.nodes('//p:Object') AS c(n) -) -UPDATE p -SET is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); +UPDATE s +SET set_options = set_options.ansi_set_options +FROM #stored_proc_info AS s +JOIN ( + SELECT x.SqlHandle, + N'SET ANSI_NULLS ' + CASE WHEN [ANSI_NULLS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_PADDING ' + CASE WHEN [ANSI_PADDING] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_WARNINGS ' + CASE WHEN [ANSI_WARNINGS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ARITHABORT ' + CASE WHEN [ARITHABORT] = 'true' THEN N'ON ' ELSE N' OFF ' END + NCHAR(10) + + N'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN [CONCAT_NULL_YIELDS_NULL] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET NUMERIC_ROUNDABORT ' + CASE WHEN [NUMERIC_ROUNDABORT] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET QUOTED_IDENTIFIER ' + CASE WHEN [QUOTED_IDENTIFIER] = 'true' THEN N'ON ' ELSE N'OFF ' + NCHAR(10) END AS [ansi_set_options] + FROM ( + SELECT + s.SqlHandle, + so.o.value('@ANSI_NULLS', 'NVARCHAR(20)') AS [ANSI_NULLS], + so.o.value('@ANSI_PADDING', 'NVARCHAR(20)') AS [ANSI_PADDING], + so.o.value('@ANSI_WARNINGS', 'NVARCHAR(20)') AS [ANSI_WARNINGS], + so.o.value('@ARITHABORT', 'NVARCHAR(20)') AS [ARITHABORT], + so.o.value('@CONCAT_NULL_YIELDS_NULL', 'NVARCHAR(20)') AS [CONCAT_NULL_YIELDS_NULL], + so.o.value('@NUMERIC_ROUNDABORT', 'NVARCHAR(20)') AS [NUMERIC_ROUNDABORT], + so.o.value('@QUOTED_IDENTIFIER', 'NVARCHAR(20)') AS [QUOTED_IDENTIFIER] + FROM #statements AS s + CROSS APPLY s.statement.nodes('//p:StatementSetOptions') AS so(o) + ) AS x +) AS set_options ON set_options.SqlHandle = s.SqlHandle +OPTION(RECOMPILE); -RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) + +RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + CASE WHEN spi.proc_name <> 'Statement' + THEN N'The stored procedure ' + spi.proc_name + ELSE N'This ad hoc statement' + END + + N' had the following implicit conversions: ' + + CHAR(10) + + STUFF(( + SELECT DISTINCT + @nl + + CASE WHEN spi2.variable_name <> N'**no_variable**' + THEN N'The variable ' + WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN N'The compiled value ' + WHEN spi2.column_name LIKE '%Expr%' + THEN 'The expression ' + ELSE N'The column ' + END + + CASE WHEN spi2.variable_name <> N'**no_variable**' + THEN spi2.variable_name + WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN spi2.compile_time_value + ELSE spi2.column_name + END + + N' has a data type of ' + + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to + ELSE spi2.variable_datatype + END + + N' which caused implicit conversion on the column ' + + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' + THEN spi2.converted_column_name + WHEN spi2.column_name = N'**no_column**' + THEN spi2.converted_column_name + WHEN spi2.converted_column_name = N'**no_column**' + THEN spi2.column_name + WHEN spi2.column_name <> spi2.converted_column_name + THEN spi2.converted_column_name + ELSE spi2.column_name + END + + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN N'' + WHEN spi2.column_name LIKE '%Expr%' + THEN N'' + WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') + AND spi2.compile_time_value <> spi2.column_name + THEN ' with the value ' + RTRIM(spi2.compile_time_value) + ELSE N'' + END + + '.' + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS implicit_conversion_info +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name ) -UPDATE p -SET p.function_count = x.function_count, - p.clr_function_count = x.clr_function_count -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID +UPDATE b +SET b.implicit_conversion_info = pk.implicit_conversion_info +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID OPTION (RECOMPILE); -RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET key_lookup_cost = x.key_lookup_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET remote_query_cost = x.remote_query_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET sort_cost = (x.sort_io + x.sort_cpu) -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM ##bou_BlitzCacheProcs b -JOIN #statements AS qs -ON b.SqlHandle = qs.SqlHandle -CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +RAISERROR(N'Updating cached parameter XML for stored procs', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + set_options + + @nl + + @nl + + N'EXEC ' + + spi.proc_name + + N' ' + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN @nl + N'More info on possible reasons: https://www.brentozar.com/go/noplans ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + AND spi2.proc_name <> N'Statement' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options +) UPDATE b -SET -b.is_table_scan = x.is_table_scan, -b.backwards_scan = x.backwards_scan, -b.forced_index = x.forced_index, -b.forced_seek = x.forced_seek, -b.forced_scan = x.forced_scan -FROM ##bou_BlitzCacheProcs b -JOIN ( -SELECT - qs.SqlHandle, - 0 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:IndexScan') AS q(n) -UNION ALL -SELECT - qs.SqlHandle, - 1 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) -) AS x ON b.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_scalar = x.computed_column_function -FROM ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID +SET b.cached_execution_parameters = pk.cached_execution_parameters +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +WHERE b.QueryType <> N'Statement' OPTION (RECOMPILE); -RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_filter = x.filter_function -FROM ( -SELECT -r.SqlHandle, -c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) -) x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -IndexOps AS -( - SELECT - r.QueryHash, - c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, - c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, - c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, - c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, - c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, - c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, - c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, - c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, - c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, - c.n.exist('@PhysicalOp[.="Table Delete"]') AS td - FROM #relop AS r - CROSS APPLY r.relop.nodes('/p:RelOp') c(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) -), iops AS -( - SELECT ios.QueryHash, - SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, - SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, - SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, - SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, - SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, - SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, - SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, - SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, - SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count - FROM IndexOps AS ios - WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', - 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', - 'Table Insert', 'Table Delete', 'Table Update') - GROUP BY ios.QueryHash) +RAISERROR(N'Updating cached parameter XML for statements', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + set_options + + @nl + + @nl + + N' See QueryText column for full query text' + + @nl + + @nl + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN @nl + N' More info on possible reasons: https://www.brentozar.com/go/noplans ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + AND spi2.proc_name = N'Statement' + AND spi2.variable_name NOT LIKE N'%msparam%' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options +) UPDATE b -SET b.index_insert_count = iops.index_insert_count, - b.index_update_count = iops.index_update_count, - b.index_delete_count = iops.index_delete_count, - b.cx_insert_count = iops.cx_insert_count, - b.cx_update_count = iops.cx_update_count, - b.cx_delete_count = iops.cx_delete_count, - b.table_insert_count = iops.table_insert_count, - b.table_update_count = iops.table_update_count, - b.table_delete_count = iops.table_delete_count -FROM ##bou_BlitzCacheProcs AS b -JOIN iops ON iops.QueryHash = b.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_spatial = x.is_spatial -FROM ( -SELECT qs.SqlHandle, - 1 AS is_spatial -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) -WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID +SET b.cached_execution_parameters = pk.cached_execution_parameters +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +WHERE b.QueryType = N'Statement' OPTION (RECOMPILE); -RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( - 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -, selects -AS ( SELECT s.QueryHash - FROM #statements AS s - WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) -, spools -AS ( SELECT DISTINCT r.QueryHash, - c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, - c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, - c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds -FROM #relop AS r -JOIN selects AS s -ON s.QueryHash = r.QueryHash -CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) -WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 -) +RAISERROR(N'Filling in implicit conversion and cached plan parameter info', 0, 1) WITH NOWAIT; UPDATE b - SET b.index_spool_rows = sp.estimated_rows, - b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM ##bou_BlitzCacheProcs b -JOIN spools sp -ON sp.QueryHash = b.QueryHash -OPTION ( RECOMPILE ); - +SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL + OR CONVERT(NVARCHAR(MAX), b.implicit_conversion_info) = N'' + THEN '' + ELSE b.implicit_conversion_info END, + b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL + OR CONVERT(NVARCHAR(MAX), b.cached_execution_parameters) = N'' + THEN '' + ELSE b.cached_execution_parameters END +FROM ##BlitzCacheProcs AS b +WHERE b.SPID = @@SPID +OPTION (RECOMPILE); -/* 2012+ only */ -IF @v >= 11 -BEGIN +/*End implicit conversion and parameter info*/ - RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET is_forced_serial = 1 - FROM #query_plan qp - WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle - AND SPID = @@SPID - AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 - AND (##bou_BlitzCacheProcs.is_parallel = 0 OR ##bou_BlitzCacheProcs.is_parallel IS NULL) - OPTION (RECOMPILE); - - - RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET columnstore_row_mode = x.is_row_mode - FROM ( - SELECT - qs.SqlHandle, - relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode - FROM #relop qs - WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 - ) AS x - WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle - AND SPID = @@SPID - OPTION (RECOMPILE) ; +/*Begin Missing Index*/ +IF EXISTS ( SELECT 1/0 + FROM ##BlitzCacheProcs AS bbcp + WHERE bbcp.missing_index_count > 0 + OR bbcp.index_spool_cost > 0 + OR bbcp.index_spool_rows > 0 + AND bbcp.SPID = @@SPID ) + + BEGIN + RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_xml + SELECT qp.QueryHash, + qp.SqlHandle, + c.mg.value('@Impact', 'FLOAT') AS Impact, + c.mg.query('.') AS cmg + FROM #query_plan AS qp + CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) + WHERE qp.QueryHash IS NOT NULL + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_schema + SELECT mix.QueryHash, mix.SqlHandle, mix.impact, + c.mi.value('@Database', 'NVARCHAR(128)'), + c.mi.value('@Schema', 'NVARCHAR(128)'), + c.mi.value('@Table', 'NVARCHAR(128)'), + c.mi.query('.') + FROM #missing_index_xml AS mix + CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_usage + SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, + c.cg.value('@Usage', 'NVARCHAR(128)'), + c.cg.query('.') + FROM #missing_index_schema ms + CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_detail + SELECT miu.QueryHash, + miu.SqlHandle, + miu.impact, + miu.database_name, + miu.schema_name, + miu.table_name, + miu.usage, + c.c.value('@Name', 'NVARCHAR(128)') + FROM #missing_index_usage AS miu + CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) + OPTION (RECOMPILE); + + RAISERROR(N'Inserting to missing indexes to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + ( QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool ) + SELECT DISTINCT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'EQUALITY' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'INEQUALITY' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'INCLUDE' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS [include], + bbcp.ExecutionCount, + bbcp.QueryPlanCost, + bbcp.PlanCreationTimeHours, + 0 as is_spool + FROM #missing_index_detail AS m + JOIN ##BlitzCacheProcs AS bbcp + ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + OPTION (RECOMPILE); + + RAISERROR(N'Inserting to #index_spool_ugly', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + INSERT #index_spool_ugly + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours) + SELECT p.QueryHash, + p.SqlHandle, + (c.n.value('@EstimateIO', 'FLOAT') + (c.n.value('@EstimateCPU', 'FLOAT'))) + / ( 1 * NULLIF(p.QueryPlanCost, 0)) * 100 AS impact, + o.n.value('@Database', 'NVARCHAR(128)') AS output_database, + o.n.value('@Schema', 'NVARCHAR(128)') AS output_schema, + o.n.value('@Table', 'NVARCHAR(128)') AS output_table, + k.n.value('@Column', 'NVARCHAR(128)') AS range_column, + e.n.value('@Column', 'NVARCHAR(128)') AS expression_column, + o.n.value('@Column', 'NVARCHAR(128)') AS output_column, + p.ExecutionCount, + p.QueryPlanCost, + p.PlanCreationTimeHours + FROM #relop AS r + JOIN ##BlitzCacheProcs p + ON p.QueryHash = r.QueryHash + CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) + CROSS APPLY r.relop.nodes('/p:RelOp/p:OutputList/p:ColumnReference') AS o(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeColumns/p:ColumnReference') AS k(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeExpressions/p:ColumnReference') AS e(n) + WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 + + RAISERROR(N'Inserting to spools to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool) + SELECT DISTINCT + isu.QueryHash, + isu.SqlHandle, + isu.impact, + isu.database_name, + isu.schema_name, + isu.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.equality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.equality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.inequality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.inequality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.include, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.include IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS include, + isu.executions, + isu.query_cost, + isu.creation_hours, + 1 AS is_spool + FROM #index_spool_ugly AS isu + + + RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; + WITH missing AS ( + SELECT DISTINCT + mip.QueryHash, + mip.SqlHandle, + mip.executions, + N'' + AS full_details + FROM #missing_index_pretty AS mip + ) + UPDATE bbcp + SET bbcp.missing_indexes = m.full_details + FROM ##BlitzCacheProcs AS bbcp + JOIN missing AS m + ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + AND m.executions = bbcp.ExecutionCount + AND SPID = @@SPID + OPTION (RECOMPILE); + + END; + + RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; + UPDATE b + SET b.missing_indexes = + CASE WHEN b.missing_indexes IS NULL + THEN '' + ELSE b.missing_indexes + END + FROM ##BlitzCacheProcs AS b + WHERE b.SPID = @@SPID + OPTION (RECOMPILE); + +/*End Missing Index*/ + + +/* Set configuration values */ +RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; +DECLARE @execution_threshold INT = 1000 , + @parameter_sniffing_warning_pct TINYINT = 30, + /* This is in average reads */ + @parameter_sniffing_io_threshold BIGINT = 100000 , + @ctp_threshold_pct TINYINT = 10, + @long_running_query_warning_seconds BIGINT = 300 * 1000 , + @memory_grant_warning_percent INT = 10; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'frequent execution threshold' = LOWER(parameter_name)) +BEGIN + SELECT @execution_threshold = CAST(value AS INT) + FROM #configuration + WHERE 'frequent execution threshold' = LOWER(parameter_name) ; + + SET @msg = ' Setting "frequent execution threshold" to ' + CAST(@execution_threshold AS VARCHAR(10)) ; + RAISERROR(@msg, 0, 1) WITH NOWAIT; END; -/* 2014+ only */ -IF @v >= 12 +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing variance percent' = LOWER(parameter_name)) BEGIN - RAISERROR('Checking for downlevel cardinality estimators being used on SQL Server 2014.', 0, 1) WITH NOWAIT; + SELECT @parameter_sniffing_warning_pct = CAST(value AS TINYINT) + FROM #configuration + WHERE 'parameter sniffing variance percent' = LOWER(parameter_name) ; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; -END ; + SET @msg = ' Setting "parameter sniffing variance percent" to ' + CAST(@parameter_sniffing_warning_pct AS VARCHAR(3)) ; -/* 2016+ only */ -IF @v >= 13 + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing io threshold' = LOWER(parameter_name)) BEGIN - RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; + SELECT @parameter_sniffing_io_threshold = CAST(value AS BIGINT) + FROM #configuration + WHERE 'parameter sniffing io threshold' = LOWER(parameter_name) ; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET p.is_row_level = 1 - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 - OPTION (RECOMPILE) ; -END ; + SET @msg = ' Setting "parameter sniffing io threshold" to ' + CAST(@parameter_sniffing_io_threshold AS VARCHAR(10)); -/* 2017+ only */ -IF @v >= 14 + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name)) BEGIN + SELECT @ctp_threshold_pct = CAST(value AS TINYINT) + FROM #configuration + WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name) ; -RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #stats_agg -SELECT qp.SqlHandle, - x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, - x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] -FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c); + SET @msg = ' Setting "cost threshold for parallelism warning" to ' + CAST(@ctp_threshold_pct AS VARCHAR(3)); -RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; -WITH stale_stats AS ( - SELECT sa.SqlHandle - FROM #stats_agg AS sa - GROUP BY sa.SqlHandle - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000 -) -UPDATE b -SET stale_stats = 1 -FROM ##bou_BlitzCacheProcs b -JOIN stale_stats os -ON b.SqlHandle = os.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE); + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -aj AS ( - SELECT - SqlHandle - FROM #relop AS r - CROSS APPLY r.relop.nodes('//p:RelOp') x(c) - WHERE x.c.exist('@IsAdaptive[.=1]') = 1 -) -UPDATE b -SET b.is_adaptive = 1 -FROM ##bou_BlitzCacheProcs b -JOIN aj -ON b.SqlHandle = aj.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE) ; +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'long running query warning (seconds)' = LOWER(parameter_name)) +BEGIN + SELECT @long_running_query_warning_seconds = CAST(value * 1000 AS BIGINT) + FROM #configuration + WHERE 'long running query warning (seconds)' = LOWER(parameter_name) ; + + SET @msg = ' Setting "long running query warning (seconds)" to ' + CAST(@long_running_query_warning_seconds AS VARCHAR(10)); + RAISERROR(@msg, 0, 1) WITH NOWAIT; END; --- query level checks -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') , - SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , - SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), - CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , - CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , - CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , - CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float') -FROM #query_plan qp -WHERE qp.QueryHash = ##bou_BlitzCacheProcs.QueryHash -AND SPID = @@SPID -OPTION (RECOMPILE); +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'unused memory grant' = LOWER(parameter_name)) +BEGIN + SELECT @memory_grant_warning_percent = CAST(value AS INT) + FROM #configuration + WHERE 'unused memory grant' = LOWER(parameter_name) ; + SET @msg = ' Setting "unused memory grant" to ' + CAST(@memory_grant_warning_percent AS VARCHAR(10)); -/* END Testing using XML nodes to speed up processing */ -RAISERROR(N'Gathering additional plan level information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans, - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END , - is_trivial = CASE WHEN QueryPlan.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 THEN 1 END -FROM ( -SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash -FROM ##bou_BlitzCacheProcs -WHERE SPID = @@SPID -GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; -/* Update to grab stored procedure name for individual statements */ -RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; -UPDATE p -SET QueryType = QueryType + ' (parent ' + - + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) - + '.' - + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' -FROM ##bou_BlitzCacheProcs p - JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle -WHERE QueryType = 'Statement' -AND SPID = @@SPID -OPTION (RECOMPILE) ; +DECLARE @ctp INT ; -/* Trace Flag Checks 2014 SP2 and 2016 SP1 only)*/ -IF @v >= 11 -BEGIN -RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, tf_pretty AS ( -SELECT qp.QueryHash, - qp.SqlHandle, - q.n.value('@Value', 'INT') AS trace_flag, - q.n.value('@Scope', 'VARCHAR(10)') AS scope -FROM #query_plan qp -CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) -) -INSERT INTO #trace_flags -SELECT DISTINCT tf1.SqlHandle , tf1.QueryHash, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Global' - FOR XML PATH(N'')), 1, 2, N'' - ) AS global_trace_flags, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Session' - FOR XML PATH(N'')), 1, 2, N'' - ) AS session_trace_flags -FROM tf_pretty AS tf1 +SELECT @ctp = NULLIF(CAST(value AS INT), 0) +FROM sys.configurations +WHERE name = 'cost threshold for parallelism' OPTION (RECOMPILE); -UPDATE p -SET p.trace_flags_session = tf.session_trace_flags -FROM ##bou_BlitzCacheProcs p -JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); -END; +/* Update to populate checks columns */ +RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; -RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -is_paul_white_electric AS ( -SELECT 1 AS [is_paul_white_electric], -r.SqlHandle -FROM #relop AS r -CROSS APPLY r.relop.nodes('//p:RelOp') c(n) -WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 -) -UPDATE b -SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM ##bou_BlitzCacheProcs AS b -JOIN is_paul_white_electric ipwe -ON ipwe.SqlHandle = b.SqlHandle -WHERE b.SPID = @@SPID +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , + parameter_sniffing = CASE WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , + near_parallel = CASE WHEN is_parallel <> 1 AND QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, + long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 + WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 + WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, + is_key_lookup_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, + is_sort_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, + is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, + is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, + long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 AND AverageCPU < 500. THEN 1 END, + low_cost_high_cpu = CASE WHEN QueryPlanCost <= 10 AND AverageCPU > 5000. THEN 1 END, + is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, + is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, + is_table_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND table_spool_cost >= QueryPlanCost / 4 THEN 1 END, + is_table_spool_more_rows = CASE WHEN table_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, + is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 1000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 1000) THEN 1 END, + is_big_spills = CASE WHEN (AvgSpills / 128.) > 499. THEN 1 END +WHERE SPID = @@SPID OPTION (RECOMPILE); -IF EXISTS ( SELECT 1 - FROM ##bou_BlitzCacheProcs AS bbcp - WHERE bbcp.implicit_conversions = 1 - OR bbcp.QueryType LIKE '%Procedure or Function: %') -BEGIN -RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; -RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #variable_info ( SPID, QueryHash, SqlHandle, proc_name, variable_name, variable_datatype, compile_time_value ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - q.n.value('@Column', 'NVARCHAR(128)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(128)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(1000)') AS compile_time_value -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) -WHERE b.SPID = @@SPID -OPTION ( RECOMPILE ); +RAISERROR('Checking for forced parameterization and cursors.', 0, 1) WITH NOWAIT; -RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #conversion_info ( SPID, QueryHash, SqlHandle, proc_name, expression ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - qq.c.value('@Expression', 'NVARCHAR(128)') AS expression -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) -WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 - AND qp.QueryHash IS NOT NULL - AND b.implicit_conversions = 1 -AND b.SPID = @@SPID -OPTION ( RECOMPILE ); +/* Set options checks */ +UPDATE p + SET is_forced_parameterized = CASE WHEN (CAST(pa.value AS INT) & 131072 = 131072) THEN 1 END , + is_forced_plan = CASE WHEN (CAST(pa.value AS INT) & 4 = 4) THEN 1 END , + SetOptions = SUBSTRING( + CASE WHEN (CAST(pa.value AS INT) & 1 = 1) THEN ', ANSI_PADDING' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 8 = 8) THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 16 = 16) THEN ', ANSI_WARNINGS' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 32 = 32) THEN ', ANSI_NULLS' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 64 = 64) THEN ', QUOTED_IDENTIFIER' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END + , 2, 200000) +FROM ##BlitzCacheProcs p + CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa +WHERE pa.attribute = 'set_options' +AND SPID = @@SPID +OPTION (RECOMPILE); -RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; -INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) -SELECT @@SPID AS SPID, - ci.SqlHandle, - ci.QueryHash, - ci.proc_name, - CASE WHEN ci.at_charindex > 0 - AND ci.bracket_charindex > 0 - THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) - ELSE N'**no_variable**' - END AS variable_name, - N'**no_variable**' AS variable_datatype, - CASE WHEN ci.at_charindex = 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column**' - END AS converted_column_name, - CASE WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex = 0 - THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) - WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex > 0 - THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) - WHEN ci.at_charindex > 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column **' - END AS column_name, - CASE WHEN ci.paren_charindex > 0 - 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 - 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 -FROM #conversion_info AS ci -OPTION ( RECOMPILE ); -IF EXISTS ( SELECT * - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name ) - BEGIN - RAISERROR(N'Updating variables', 0, 1) WITH NOWAIT; - UPDATE sp - SET sp.variable_datatype = vi.variable_datatype, - sp.compile_time_value = vi.compile_time_value - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); - END - ELSE - BEGIN - RAISERROR(N'Inserting variables', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, variable_name, variable_datatype, compile_time_value, proc_name ) - SELECT vi.SPID, vi.SqlHandle, vi.QueryHash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, vi.proc_name - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - END +/* Cursor checks */ +UPDATE p +SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END +FROM ##BlitzCacheProcs p + CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa +WHERE pa.attribute LIKE '%cursor%' +AND SPID = @@SPID +OPTION (RECOMPILE); -RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; -UPDATE s -SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) - ELSE s.variable_datatype - END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) - ELSE s.converted_to - END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) - WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' THEN - QUOTENAME(compile_time_value, '''') - ELSE s.compile_time_value - END -FROM #stored_proc_info AS s -OPTION(RECOMPILE); +UPDATE p +SET is_cursor = 1 +FROM ##BlitzCacheProcs p +WHERE QueryHash = 0x0000000000000000 +OR QueryPlanHash = 0x0000000000000000 +AND SPID = @@SPID +OPTION (RECOMPILE); -RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, - CONVERT(XML, - N' 'Statement' - THEN N'The stored procedure ' + spi.proc_name - ELSE N'This ad hoc statement' - END - + N' had the following implicit conversions: ' - + CHAR(10) - + STUFF(( - SELECT DISTINCT - @nl - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN N'The variable ' - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'The compiled value ' - WHEN spi2.column_name LIKE '%Expr%' - THEN 'The expression ' - ELSE N'The column ' - END - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN spi2.variable_name - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN spi2.compile_time_value - ELSE spi2.column_name - END - + N' has a data type of ' - + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to - ELSE spi2.variable_datatype - END - + N' which caused implicit conversion on the column ' - + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' - THEN spi2.converted_column_name - WHEN spi2.column_name = N'**no_column**' - THEN spi2.converted_column_name - WHEN spi2.converted_column_name = N'**no_column**' - THEN spi2.column_name - WHEN spi2.column_name <> spi2.converted_column_name - THEN spi2.converted_column_name - ELSE spi2.column_name - END - + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'' - WHEN spi2.column_name LIKE '%Expr%' - THEN N'' - WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') - THEN ' with the value ' + RTRIM(spi2.compile_time_value) - ELSE N'' - END - + '.' - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) + +RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; +/* Populate warnings */ +UPDATE ##BlitzCacheProcs +SET Warnings = SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + + CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + + CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + + CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + + CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + + CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + + CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + + CASE WHEN is_cursor = 1 THEN ', Cursor' + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END + ELSE '' END + + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + + CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + + CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + + CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + + CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + + CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR Function(s)' ELSE '' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + + CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + + CASE WHEN is_table_scan = 1 THEN ', Table Scans (Heaps)' ELSE '' END + + CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + + CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + + CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + + CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + + CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + + CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + + CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + + CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + + CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + + CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + + CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + + CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + + CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + + CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + + CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + + CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + + CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb Spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) +WHERE SPID = @@SPID +OPTION (RECOMPILE); + + +RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; +WITH statement_warnings AS + ( +SELECT DISTINCT + SqlHandle, + Warnings = SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + + CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + + CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + + CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + + CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + + --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + + CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + + CASE WHEN is_cursor = 1 THEN ', Cursor' + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END + ELSE '' END + + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + + CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + + CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + + CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + + CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + + CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' CLR Function(s)' ELSE '' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + + CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + + CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + + CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + + CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + + CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + + CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + + CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + + CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + + CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + + CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + + CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + + CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + + CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + + CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + + CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + + CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + + CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + + CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + + CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) +FROM ##BlitzCacheProcs b +WHERE SPID = @@SPID +AND QueryType LIKE 'Statement (parent%' + ) UPDATE b -SET b.implicit_conversion_info = pk.implicit_conversion_info -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); +SET b.Warnings = s.Warnings +FROM ##BlitzCacheProcs AS b +JOIN statement_warnings s +ON b.SqlHandle = s.SqlHandle +WHERE QueryType LIKE 'Procedure or Function%' +AND SPID = @@SPID +OPTION (RECOMPILE); -RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, -CONVERT(XML, - N' N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' - THEN spi2.variable_name + N' = ' - ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' - END - + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' - THEN @nl + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' - WHEN spi2.compile_time_value = N'NULL' - THEN spi2.compile_time_value - ELSE RTRIM(spi2.compile_time_value) - END - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - AND spi2.proc_name <> N'Statement' - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @nl - + N' -- ?>' - ) AS cached_execution_parameters -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) +RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; +WITH plan_handle AS ( +SELECT b.PlanHandle +FROM ##BlitzCacheProcs b + CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp + CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp + WHERE tqp.encrypted = 0 + AND b.SPID = @@SPID + AND (qp.query_plan IS NULL + AND tqp.query_plan IS NOT NULL) +) UPDATE b -SET b.cached_execution_parameters = pk.cached_execution_parameters -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); +SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' + , 'We couldn''t find a plan for this query. More info on possible reasons: https://www.brentozar.com/go/noplans') +FROM ##BlitzCacheProcs b +LEFT JOIN plan_handle ph ON +b.PlanHandle = ph.PlanHandle +WHERE b.QueryPlan IS NULL +AND b.SPID = @@SPID +OPTION (RECOMPILE); +RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET Warnings = 'No warnings detected. ' + CASE @ExpertMode + WHEN 0 + THEN ' Try running sp_BlitzCache with @ExpertMode = 1 to find more advanced problems.' + ELSE '' + END +WHERE Warnings = '' OR Warnings IS NULL +AND SPID = @@SPID +OPTION (RECOMPILE); -END; --End implicit conversion information gathering -UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN '' ELSE b.implicit_conversion_info END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN '' ELSE b.cached_execution_parameters END -FROM ##bou_BlitzCacheProcs AS b -WHERE b.SPID = @@SPID -OPTION(RECOMPILE); +Results: +IF @ExportToExcel = 1 +BEGIN + RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; -/*Begin Missing Index*/ + /* excel output */ + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) + OPTION(RECOMPILE); -IF EXISTS - (SELECT 1 FROM ##bou_BlitzCacheProcs AS bbcp WHERE bbcp.missing_index_count > 0 AND bbcp.SPID = @@SPID) - BEGIN + SET @sql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT TOP (@Top) + DatabaseName AS [Database Name], + QueryPlanCost AS [Cost], + QueryText, + QueryType AS [Query Type], + Warnings, + ExecutionCount, + ExecutionsPerMinute AS [Executions / Minute], + PercentExecutions AS [Execution Weight], + PercentExecutionsByType AS [% Executions (Type)], + SerialDesiredMemory AS [Serial Desired Memory], + SerialRequiredMemory AS [Serial Required Memory], + TotalCPU AS [Total CPU (ms)], + AverageCPU AS [Avg CPU (ms)], + PercentCPU AS [CPU Weight], + PercentCPUByType AS [% CPU (Type)], + TotalDuration AS [Total Duration (ms)], + AverageDuration AS [Avg Duration (ms)], + PercentDuration AS [Duration Weight], + PercentDurationByType AS [% Duration (Type)], + TotalReads AS [Total Reads], + AverageReads AS [Average Reads], + PercentReads AS [Read Weight], + PercentReadsByType AS [% Reads (Type)], + TotalWrites AS [Total Writes], + AverageWrites AS [Average Writes], + PercentWrites AS [Write Weight], + PercentWritesByType AS [% Writes (Type)], + TotalReturnedRows, + AverageReturnedRows, + MinReturnedRows, + MaxReturnedRows, + MinGrantKB, + MaxGrantKB, + MinUsedGrantKB, + MaxUsedGrantKB, + PercentMemoryGrantUsed, + AvgMaxMemoryGrant, + MinSpills, + MaxSpills, + TotalSpills, + AvgSpills, + NumberOfPlans, + NumberOfDistinctPlans, + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + StatementStartOffset, + StatementEndOffset, + PlanGenerationNum, + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + QueryHash, + QueryPlanHash, + COALESCE(SetOptions, '''') AS [SET Options] + FROM ##BlitzCacheProcs + WHERE 1 = 1 + AND SPID = @@SPID ' + @nl; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_xml - SELECT qp.QueryHash, - qp.SqlHandle, - c.mg.value('@Impact', 'FLOAT') AS Impact, - c.mg.query('.') AS cmg - FROM #query_plan AS qp - CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) - WHERE qp.QueryHash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_schema - SELECT mix.QueryHash, mix.SqlHandle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , - c.mi.query('.') - FROM #missing_index_xml AS mix - CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_usage - SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, - c.cg.value('@Usage', 'NVARCHAR(128)'), - c.cg.query('.') - FROM #missing_index_schema ms - CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_detail - SELECT miu.QueryHash, - miu.SqlHandle, - miu.impact, - miu.database_name, - miu.schema_name, - miu.table_name, - miu.usage, - c.c.value('@Name', 'NVARCHAR(128)') - FROM #missing_index_usage AS miu - CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); - - INSERT #missing_index_pretty - SELECT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'EQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INEQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INCLUDE' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] - FROM #missing_index_detail AS m - GROUP BY m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); - - WITH missing AS ( - SELECT mip.QueryHash, - mip.SqlHandle, - CONVERT(XML, - N'' - ) AS full_details - FROM #missing_index_pretty AS mip - GROUP BY mip.QueryHash, mip.SqlHandle, mip.impact - ) - UPDATE bbcp - SET bbcp.missing_indexes = m.full_details - FROM ##bou_BlitzCacheProcs AS bbcp - JOIN missing AS m - ON m.SqlHandle = bbcp.SqlHandle - AND SPID = @@SPID - OPTION(RECOMPILE); + IF @MinutesBack IS NOT NULL + BEGIN + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; - - END + SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'duplicate' THEN N' plan_multiple_plans ' /* Issue #3345 */ + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + END + N' DESC '; - UPDATE b - SET b.missing_indexes = - CASE WHEN b.missing_indexes IS NULL - THEN '' - ELSE b.missing_indexes - END - FROM ##bou_BlitzCacheProcs AS b - WHERE b.SPID = @@SPID - OPTION(RECOMPILE); + SET @sql += N' OPTION (RECOMPILE) ; '; -/*End Missing Index*/ + IF @sql IS NULL + BEGIN + RAISERROR('@sql is null, which means dynamic SQL generation went terribly wrong', 0, 1) WITH NOWAIT; + END + IF @Debug = 1 + BEGIN + RAISERROR('Printing @sql, the dynamic SQL we generated:', 0, 1) WITH NOWAIT; + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; +END; -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; - GOTO Results ; - END; +RAISERROR('Displaying analysis of plan cache.', 0, 1) WITH NOWAIT; -/* Set configuration values */ -RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; -DECLARE @execution_threshold INT = 1000 , - @parameter_sniffing_warning_pct TINYINT = 30, - /* This is in average reads */ - @parameter_sniffing_io_threshold BIGINT = 100000 , - @ctp_threshold_pct TINYINT = 10, - @long_running_query_warning_seconds BIGINT = 300 * 1000 , - @memory_grant_warning_percent INT = 10; +DECLARE @columns NVARCHAR(MAX) = N'' ; -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'frequent execution threshold' = LOWER(parameter_name)) +IF @ExpertMode = 0 BEGIN - SELECT @execution_threshold = CAST(value AS INT) - FROM #configuration - WHERE 'frequent execution threshold' = LOWER(parameter_name) ; - - SET @msg = ' Setting "frequent execution threshold" to ' + CAST(@execution_threshold AS VARCHAR(10)) ; - - RAISERROR(@msg, 0, 1) WITH NOWAIT; + RAISERROR(N'Returning ExpertMode = 0', 0, 1) WITH NOWAIT; + SET @columns = N' DatabaseName AS [Database], + QueryPlanCost AS [Cost], + QueryText AS [Query Text], + QueryType AS [Query Type], + Warnings AS [Warnings], + QueryPlan AS [Query Plan], + missing_indexes AS [Missing Indexes], + implicit_conversion_info AS [Implicit Conversion Info], + cached_execution_parameters AS [Cached Execution Parameters], + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Avg Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Avg Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Average Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + LastCompletionTime AS [Last Completion], + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + COALESCE(SetOptions, '''') AS [SET Options], + QueryHash AS [Query Hash], + PlanGenerationNum, + [Remove Plan Handle From Cache]'; END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing variance percent' = LOWER(parameter_name)) +ELSE BEGIN - SELECT @parameter_sniffing_warning_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'parameter sniffing variance percent' = LOWER(parameter_name) ; - - SET @msg = ' Setting "parameter sniffing variance percent" to ' + CAST(@parameter_sniffing_warning_pct AS VARCHAR(3)) ; + SET @columns = N' DatabaseName AS [Database], + QueryPlanCost AS [Cost], + QueryText AS [Query Text], + QueryType AS [Query Type], + Warnings AS [Warnings], + QueryPlan AS [Query Plan], + missing_indexes AS [Missing Indexes], + implicit_conversion_info AS [Implicit Conversion Info], + cached_execution_parameters AS [Cached Execution Parameters], ' + @nl; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + IF @ExpertMode = 2 /* Opserver */ + BEGIN + RAISERROR(N'Returning Expert Mode = 2', 0, 1) WITH NOWAIT; + SET @columns += N' + SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN '', 20'' ELSE '''' END + + CASE WHEN compile_timeout = 1 THEN '', 18'' ELSE '''' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN '', 19'' ELSE '''' END + + CASE WHEN busy_loops = 1 THEN '', 16'' ELSE '''' END + + CASE WHEN is_forced_plan = 1 THEN '', 3'' ELSE '''' END + + CASE WHEN is_forced_parameterized > 0 THEN '', 5'' ELSE '''' END + + CASE WHEN unparameterized_query = 1 THEN '', 23'' ELSE '''' END + + CASE WHEN missing_index_count > 0 THEN '', 10'' ELSE '''' END + + CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + + CASE WHEN is_cursor = 1 THEN '', 4'' ELSE '''' END + + CASE WHEN is_parallel = 1 THEN '', 6'' ELSE '''' END + + CASE WHEN near_parallel = 1 THEN '', 7'' ELSE '''' END + + CASE WHEN frequent_execution = 1 THEN '', 1'' ELSE '''' END + + CASE WHEN plan_warnings = 1 THEN '', 8'' ELSE '''' END + + CASE WHEN parameter_sniffing = 1 THEN '', 2'' ELSE '''' END + + CASE WHEN long_running = 1 THEN '', 9'' ELSE '''' END + + CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + + CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + + CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + + CASE WHEN plan_multiple_plans > 0 THEN '', 21'' ELSE '''' END + + CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + + CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + + CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + + CASE WHEN is_key_lookup_expensive = 1 THEN '', 26'' ELSE '''' END + + CASE WHEN is_remote_query_expensive = 1 THEN '', 28'' ELSE '''' END + + CASE WHEN trace_flags_session IS NOT NULL THEN '', 29'' ELSE '''' END + + CASE WHEN is_unused_grant = 1 THEN '', 30'' ELSE '''' END + + CASE WHEN function_count > 0 THEN '', 31'' ELSE '''' END + + CASE WHEN clr_function_count > 0 THEN '', 32'' ELSE '''' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN '', 33'' ELSE '''' END + + CASE WHEN is_table_variable = 1 THEN '', 34'' ELSE '''' END + + CASE WHEN no_stats_warning = 1 THEN '', 35'' ELSE '''' END + + CASE WHEN relop_warnings = 1 THEN '', 36'' ELSE '''' END + + CASE WHEN is_table_scan = 1 THEN '', 37'' ELSE '''' END + + CASE WHEN backwards_scan = 1 THEN '', 38'' ELSE '''' END + + CASE WHEN forced_index = 1 THEN '', 39'' ELSE '''' END + + CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '', 40'' ELSE '''' END + + CASE WHEN columnstore_row_mode = 1 THEN '', 41'' ELSE '''' END + + CASE WHEN is_computed_scalar = 1 THEN '', 42'' ELSE '''' END + + CASE WHEN is_sort_expensive = 1 THEN '', 43'' ELSE '''' END + + CASE WHEN is_computed_filter = 1 THEN '', 44'' ELSE '''' END + + CASE WHEN index_ops >= 5 THEN '', 45'' ELSE '''' END + + CASE WHEN is_row_level = 1 THEN '', 46'' ELSE '''' END + + CASE WHEN is_spatial = 1 THEN '', 47'' ELSE '''' END + + CASE WHEN index_dml = 1 THEN '', 48'' ELSE '''' END + + CASE WHEN table_dml = 1 THEN '', 49'' ELSE '''' END + + CASE WHEN long_running_low_cpu = 1 THEN '', 50'' ELSE '''' END + + CASE WHEN low_cost_high_cpu = 1 THEN '', 51'' ELSE '''' END + + CASE WHEN stale_stats = 1 THEN '', 52'' ELSE '''' END + + CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + + CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + + CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + + CASE WHEN is_table_spool_expensive = 1 THEN + '', 67'' ELSE '''' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + '', 68'' ELSE '''' END + + CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + + CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END + + CASE WHEN is_row_goal = 1 THEN '', 58'' ELSE '''' END + + CASE WHEN is_big_spills = 1 THEN '', 59'' ELSE '''' END + + CASE WHEN is_mstvf = 1 THEN '', 60'' ELSE '''' END + + CASE WHEN is_mm_join = 1 THEN '', 61'' ELSE '''' END + + CASE WHEN is_nonsargable = 1 THEN '', 62'' ELSE '''' END + + CASE WHEN CompileTime > 5000 THEN '', 63 '' ELSE '''' END + + CASE WHEN CompileCPU > 5000 THEN '', 64 '' ELSE '''' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN '', 65 '' ELSE '''' END + + CASE WHEN select_with_writes > 0 THEN '', 66'' ELSE '''' END + , 3, 200000) AS opserver_warning , ' + @nl ; + END; + + SET @columns += N' + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((SerialDesiredMemory) AS BIGINT), 1) AS [Serial Desired Memory], + CONVERT(NVARCHAR(30), CAST((SerialRequiredMemory) AS BIGINT), 1) AS [Serial Required Memory], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Average Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Average Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((PercentExecutionsByType) AS BIGINT), 1) AS [% Executions (Type)], + CONVERT(NVARCHAR(30), CAST((PercentCPUByType) AS BIGINT), 1) AS [% CPU (Type)], + CONVERT(NVARCHAR(30), CAST((PercentDurationByType) AS BIGINT), 1) AS [% Duration (Type)], + CONVERT(NVARCHAR(30), CAST((PercentReadsByType) AS BIGINT), 1) AS [% Reads (Type)], + CONVERT(NVARCHAR(30), CAST((PercentWritesByType) AS BIGINT), 1) AS [% Writes (Type)], + CONVERT(NVARCHAR(30), CAST((TotalReturnedRows) AS BIGINT), 1) AS [Total Rows], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Avg Rows], + CONVERT(NVARCHAR(30), CAST((MinReturnedRows) AS BIGINT), 1) AS [Min Rows], + CONVERT(NVARCHAR(30), CAST((MaxReturnedRows) AS BIGINT), 1) AS [Max Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], + CONVERT(NVARCHAR(30), CAST((NumberOfPlans) AS BIGINT), 1) AS [# Plans], + CONVERT(NVARCHAR(30), CAST((NumberOfDistinctPlans) AS BIGINT), 1) AS [# Distinct Plans], + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + LastCompletionTime AS [Last Completion], + CONVERT(NVARCHAR(30), CAST((CachedPlanSize) AS BIGINT), 1) AS [Cached Plan Size (KB)], + CONVERT(NVARCHAR(30), CAST((CompileTime) AS BIGINT), 1) AS [Compile Time (ms)], + CONVERT(NVARCHAR(30), CAST((CompileCPU) AS BIGINT), 1) AS [Compile CPU (ms)], + CONVERT(NVARCHAR(30), CAST((CompileMemory) AS BIGINT), 1) AS [Compile memory (KB)], + COALESCE(SetOptions, '''') AS [SET Options], + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + [SQL Handle More Info], + QueryHash AS [Query Hash], + [Query Hash More Info], + QueryPlanHash AS [Query Plan Hash], + StatementStartOffset, + StatementEndOffset, + PlanGenerationNum, + [Remove Plan Handle From Cache], + [Remove SQL Handle From Cache]'; END; -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing io threshold' = LOWER(parameter_name)) -BEGIN - SELECT @parameter_sniffing_io_threshold = CAST(value AS BIGINT) - FROM #configuration - WHERE 'parameter sniffing io threshold' = LOWER(parameter_name) ; +SET @sql = N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +SELECT TOP (@Top) ' + @columns + @nl + N' +FROM ##BlitzCacheProcs +WHERE SPID = @spid ' + @nl; - SET @msg = ' Setting "parameter sniffing io threshold" to ' + CAST(@parameter_sniffing_io_threshold AS VARCHAR(10)); +IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount ' + @nl; + END; - RAISERROR(@msg, 0, 1) WITH NOWAIT; +IF @MinutesBack IS NOT NULL + BEGIN + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; + END; + +SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB ' + WHEN N'duplicate' THEN N' plan_multiple_plans ' + WHEN N'spills' THEN N' MaxSpills ' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + END + N' DESC '; +SET @sql += N' OPTION (RECOMPILE) ; '; + +IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; +IF(@OutputType <> 'NONE') +BEGIN + EXEC sp_executesql @sql, N'@Top INT, @spid INT, @MinimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; END; -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name)) +/* + +This section will check if: + * >= 30% of plans were created in the last hour + * Check on the memory_clerks DMV for space used by TokenAndPermUserStore + * Compare that to the size of the buffer pool + * If it's >10%, +*/ +IF EXISTS +( + SELECT 1/0 + FROM #plan_creation AS pc + WHERE pc.percent_1 >= 30 +) BEGIN - SELECT @ctp_threshold_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name) ; - SET @msg = ' Setting "cost threshold for parallelism warning" to ' + CAST(@ctp_threshold_pct AS VARCHAR(3)); +SELECT @common_version = + CONVERT(DECIMAL(10,2), c.common_version) +FROM #checkversion AS c; - RAISERROR(@msg, 0, 1) WITH NOWAIT; +IF @common_version >= 11 + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' +ELSE + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(single_pages_kb + multi_pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' + +EXEC sys.sp_executesql @user_perm_sql, + N'@buffer_pool_memory_gb DECIMAL(10,2) OUTPUT', + @buffer_pool_memory_gb = @buffer_pool_memory_gb OUTPUT; + +IF @common_version >= 11 +BEGIN + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN (pages_kb / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), (pages_kb / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; END; -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'long running query warning (seconds)' = LOWER(parameter_name)) +IF @common_version < 11 BEGIN - SELECT @long_running_query_warning_seconds = CAST(value * 1000 AS BIGINT) - FROM #configuration - WHERE 'long running query warning (seconds)' = LOWER(parameter_name) ; + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; +END; - SET @msg = ' Setting "long running query warning (seconds)" to ' + CAST(@long_running_query_warning_seconds AS VARCHAR(10)); +EXEC sys.sp_executesql @user_perm_sql, + N'@user_perm_gb DECIMAL(10,2) OUTPUT', + @user_perm_gb = @user_perm_gb_out OUTPUT; - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; +IF @buffer_pool_memory_gb > 0 + BEGIN + IF (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100. >= 10 + BEGIN + SET @is_tokenstore_big = 1; + SET @user_perm_percent = (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100.; + END + END -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'unused memory grant' = LOWER(parameter_name)) +END + + + +IF @HideSummary = 0 AND @ExportToExcel = 0 BEGIN - SELECT @memory_grant_warning_percent = CAST(value AS INT) - FROM #configuration - WHERE 'unused memory grant' = LOWER(parameter_name) ; + IF @Reanalyze = 0 + BEGIN + RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; - SET @msg = ' Setting "unused memory grant" to ' + CAST(@memory_grant_warning_percent AS VARCHAR(10)); + /* Build summary data */ + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE frequent_execution = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 1, + 100, + 'Execution Pattern', + 'Frequent Execution', + 'https://www.brentozar.com/blitzcache/frequently-executed-queries/', + 'Queries are being executed more than ' + + CAST (@execution_threshold AS VARCHAR(5)) + + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE parameter_sniffing = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 2, + 50, + 'Parameterization', + 'Parameter Sniffing', + 'https://www.brentozar.com/blitzcache/parameter-sniffing/', + 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; -DECLARE @ctp INT ; + /* Forced execution plans */ + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_forced_plan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 3, + 50, + 'Parameterization', + 'Forced Plan', + 'https://www.brentozar.com/blitzcache/forced-plans/', + 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); -SELECT @ctp = NULLIF(CAST(value AS INT), 0) -FROM sys.configurations -WHERE name = 'cost threshold for parallelism' -OPTION (RECOMPILE); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Cursor', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_optimistic_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Optimistic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are optimistic cursors in the plan cache, which can harm performance.'); -/* Update to populate checks columns */ -RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_forward_only_cursor = 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Non-forward Only Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are non-forward only cursors in the plan cache, which can harm performance.'); -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , - parameter_sniffing = CASE WHEN AverageReads > @parameter_sniffing_io_threshold - AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , - near_parallel = CASE WHEN QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, - long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 - WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 - WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, - is_key_lookup_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, - is_sort_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, - is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, - is_forced_serial = CASE WHEN is_forced_serial = 1 THEN 1 END, - is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, - long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 THEN 1 END, - low_cost_high_cpu = CASE WHEN QueryPlanCost < @ctp AND AverageCPU > 500. AND QueryPlanCost * 10 < AverageCPU THEN 1 END, - is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, - is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, - is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 10000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 10000) THEN 1 END -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_cursor_dynamic = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Dynamic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Dynamic Cursors inhibit parallelism!.'); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_fast_forward_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Fast Forward Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Fast forward cursors inhibit parallelism!.'); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_forced_parameterized = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 5, + 50, + 'Parameterization', + 'Forced Parameterization', + 'https://www.brentozar.com/blitzcache/forced-parameterization/', + 'Execution plans have been compiled with forced parameterization.') ; -RAISERROR('Checking for forced parameterization and cursors.', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_parallel = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 6, + 200, + 'Execution Plans', + 'Parallel', + 'https://www.brentozar.com/blitzcache/parallel-plans-detected/', + 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; -/* Set options checks */ -UPDATE p - SET is_forced_parameterized = CASE WHEN (CAST(pa.value AS INT) & 131072 = 131072) THEN 1 END , - is_forced_plan = CASE WHEN (CAST(pa.value AS INT) & 4 = 4) THEN 1 END , - SetOptions = SUBSTRING( - CASE WHEN (CAST(pa.value AS INT) & 1 = 1) THEN ', ANSI_PADDING' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8 = 8) THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 16 = 16) THEN ', ANSI_WARNINGS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 32 = 32) THEN ', ANSI_NULLS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 64 = 64) THEN ', QUOTED_IDENTIFIER' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute = 'set_options' -AND SPID = @@SPID -OPTION (RECOMPILE) ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE near_parallel = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 7, + 200, + 'Execution Plans', + 'Nearly Parallel', + 'https://www.brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', + 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE plan_warnings = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 8, + 50, + 'Execution Plans', + 'Plan Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', + 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; -/* Cursor checks */ -UPDATE p -SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute LIKE '%cursor%' -AND SPID = @@SPID -OPTION (RECOMPILE) ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE long_running = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 9, + 50, + 'Performance', + 'Long Running Query', + 'https://www.brentozar.com/blitzcache/long-running-queries/', + 'Long running queries have been found. These are queries with an average duration longer than ' + + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) + + ' second(s). These queries should be investigated for additional tuning options.') ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.missing_index_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 10, + 50, + 'Performance', + 'Missing Indexes', + 'https://www.brentozar.com/blitzcache/missing-index-request/', + 'Queries found with missing indexes.'); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.downlevel_estimator = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 13, + 200, + 'Cardinality', + 'Downlevel CE', + 'https://www.brentozar.com/blitzcache/legacy-cardinality-estimator/', + 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); -RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; -/* Populate warnings */ -UPDATE ##bou_BlitzCacheProcs -SET Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; -WITH statement_warnings AS - ( -SELECT DISTINCT - SqlHandle, - Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs b -WHERE SPID = @@SPID -AND QueryType LIKE 'Statement (parent%' - ) -UPDATE b -SET b.Warnings = s.Warnings -FROM ##bou_BlitzCacheProcs AS b -JOIN statement_warnings s -ON b.SqlHandle = s.SqlHandle -WHERE QueryType LIKE 'Procedure or Function%' -AND SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; -WITH plan_handle AS ( -SELECT b.PlanHandle -FROM ##bou_BlitzCacheProcs b - CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp - CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp - WHERE tqp.encrypted = 0 - AND b.SPID = @@SPID - AND (qp.query_plan IS NULL - AND tqp.query_plan IS NOT NULL) -) -UPDATE b -SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' - , 'We couldn''t find a plan for this query. Possible reasons for this include dynamic SQL, RECOMPILE hints, and encrypted code.') -FROM ##bou_BlitzCacheProcs b -LEFT JOIN plan_handle ph ON -b.PlanHandle = ph.PlanHandle -WHERE b.QueryPlan IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET Warnings = 'No warnings detected.' -WHERE Warnings = '' OR Warnings IS NULL -AND SPID = @@SPID -OPTION (RECOMPILE); - + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE implicit_conversions = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 14, + 50, + 'Performance', + 'Implicit Conversions', + 'https://www.brentozar.com/go/implicit', + 'One or more queries are comparing two fields that are not of the same data type.') ; -Results: -IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL -BEGIN - RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE busy_loops = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 16, + 100, + 'Performance', + 'Busy Loops', + 'https://www.brentozar.com/blitzcache/busy-loops/', + 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); - /* send results to a table */ - DECLARE @insert_sql NVARCHAR(MAX) = N'' ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE tvf_join = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 17, + 50, + 'Performance', + 'Function Join', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); - SET @insert_sql = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + N'(ID bigint NOT NULL IDENTITY(1,1), - ServerName nvarchar(256), - CheckDate DATETIMEOFFSET, - Version nvarchar(256), - QueryType nvarchar(256), - Warnings varchar(max), - DatabaseName sysname, - SerialDesiredMemory float, - SerialRequiredMemory float, - AverageCPU bigint, - TotalCPU bigint, - PercentCPUByType money, - CPUWeight money, - AverageDuration bigint, - TotalDuration bigint, - DurationWeight money, - PercentDurationByType money, - AverageReads bigint, - TotalReads bigint, - ReadWeight money, - PercentReadsByType money, - AverageWrites bigint, - TotalWrites bigint, - WriteWeight money, - PercentWritesByType money, - ExecutionCount bigint, - ExecutionWeight money, - PercentExecutionsByType money,' + N' - ExecutionsPerMinute money, - PlanCreationTime datetime, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime datetime, - PlanHandle varbinary(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' - ELSE ''N/A'' END, - SqlHandle varbinary(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' - ELSE ''N/A'' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryHash binary(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryPlanHash binary(8), - StatementStartOffset int, - StatementEndOffset int, - MinReturnedRows bigint, - MaxReturnedRows bigint, - AverageReturnedRows money, - TotalReturnedRows bigint, - QueryText nvarchar(max), - QueryPlan xml, - NumberOfPlans int, - NumberOfDistinctPlans int, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryPlanCost FLOAT, - CONSTRAINT [PK_' +CAST(NEWID() AS NCHAR(36)) + '] PRIMARY KEY CLUSTERED(ID))'; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE compile_timeout = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 18, + 50, + 'Execution Plans', + 'Compilation Timeout', + 'https://www.brentozar.com/blitzcache/compilation-timeout/', + 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); - EXEC sp_executesql @insert_sql ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE compile_memory_limit_exceeded = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 19, + 50, + 'Execution Plans', + 'Compile Memory Limit Exceeded', + 'https://www.brentozar.com/blitzcache/compile-memory-limit-exceeded/', + 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); - IF @CheckDateOverride IS NULL - BEGIN - SET @CheckDateOverride = SYSDATETIMEOFFSET(); - END + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE warning_no_join_predicate = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 20, + 50, + 'Execution Plans', + 'No Join Predicate', + 'https://www.brentozar.com/blitzcache/no-join-predicate/', + 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE plan_multiple_plans > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 21, + 200, + 'Execution Plans', + 'Multiple Plans', + 'https://www.brentozar.com/blitzcache/multiple-plans/', + 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); - SET @insert_sql =N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + N''') ' - + 'INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + N' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ) ' - + N'SELECT TOP (@Top) ' - + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), N'''') + N', @CheckDateOverride, ' - + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), N'''') + ', ' - + N' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ' - + N' FROM ##bou_BlitzCacheProcs ' - + N' WHERE 1=1 '; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @insert_sql += N' AND ExecutionCount >= @MinimumExecutionCount '; - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE unmatched_index_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 22, + 100, + 'Performance', + 'Unmatched Indexes', + 'https://www.brentozar.com/blitzcache/unmatched-indexes', + 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); - IF @MinutesBack IS NOT NULL - BEGIN - SET @insert_sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE unparameterized_query = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 23, + 100, + 'Parameterization', + 'Unparameterized Query', + 'https://www.brentozar.com/blitzcache/unparameterized-queries', + 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); - SET @insert_sql += N' AND SPID = @@SPID '; - - SELECT @insert_sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; - - SET @insert_sql += N' OPTION (RECOMPILE) ; '; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_trivial = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 24, + 100, + 'Execution Plans', + 'Trivial Plans', + 'https://www.brentozar.com/blitzcache/trivial-plans', + 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_forced_serial= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 25, + 10, + 'Execution Plans', + 'Forced Serialization', + 'https://www.brentozar.com/blitzcache/forced-serialization/', + 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; - EXEC sp_executesql @insert_sql, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_key_lookup_expensive= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 26, + 100, + 'Execution Plans', + 'Expensive Key Lookup', + 'https://www.brentozar.com/blitzcache/expensive-key-lookups/', + 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; - RETURN; -END; -ELSE IF @ExportToExcel = 1 -BEGIN - RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_remote_query_expensive= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 28, + 100, + 'Execution Plans', + 'Expensive Remote Query', + 'https://www.brentozar.com/blitzcache/expensive-remote-query/', + 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; - /* excel output */ - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) - OPTION(RECOMPILE); + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.trace_flags_session IS NOT NULL + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 29, + 200, + 'Trace Flags', + 'Session Level Trace Flags Enabled', + 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', + 'Someone is enabling session level Trace Flags in a query.') ; - SET @sql = N' - SELECT TOP (@Top) - DatabaseName AS [Database Name], - QueryPlanCost AS [Cost], - QueryText, - QueryType AS [Query Type], - Warnings, - ExecutionCount, - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - PercentExecutionsByType AS [% Executions (Type)], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - PercentCPUByType AS [% CPU (Type)], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - PercentDurationByType AS [% Duration (Type)], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - PercentReadsByType AS [% Reads (Type)], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows, - AverageReturnedRows, - MinReturnedRows, - MaxReturnedRows, - MinGrantKB, - MaxGrantKB, - MinUsedGrantKB, - MaxUsedGrantKB, - PercentMemoryGrantUsed, - AvgMaxMemoryGrant, - NumberOfPlans, - NumberOfDistinctPlans, - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - StatementStartOffset, - StatementEndOffset, - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - QueryHash, - QueryPlanHash, - COALESCE(SetOptions, '''') AS [SET Options] - FROM ##bou_BlitzCacheProcs - WHERE 1 = 1 - AND SPID = @@SPID ' + @nl; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_unused_grant IS NOT NULL + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 30, + 100, + 'Memory Grant', + 'Unused Memory Grant', + 'https://www.brentozar.com/blitzcache/unused-memory-grants/', + 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.function_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 31, + 100, + 'Compute Scalar That References A Function', + 'Calls Functions', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; - SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN ' TotalCPU ' - WHEN 'reads' THEN ' TotalReads ' - WHEN 'writes' THEN ' TotalWrites ' - WHEN 'duration' THEN ' TotalDuration ' - WHEN 'executions' THEN ' ExecutionCount ' - WHEN 'compiles' THEN ' PlanCreationTime ' - WHEN 'memory grant' THEN 'MaxGrantKB' - WHEN 'avg cpu' THEN 'AverageCPU' - WHEN 'avg reads' THEN 'AverageReads' - WHEN 'avg writes' THEN 'AverageWrites' - WHEN 'avg duration' THEN 'AverageDuration' - WHEN 'avg executions' THEN 'ExecutionsPerMinute' - WHEN 'avg memory grant' THEN 'AvgMaxMemoryGrant' - END + N' DESC '; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.clr_function_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 32, + 100, + 'Compute Scalar That References A CLR Function', + 'Calls CLR Functions', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'May force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; - SET @sql += N' OPTION (RECOMPILE) ; '; - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_variable = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 33, + 100, + 'Table Variables detected', + 'Table Variables', + 'https://www.brentozar.com/blitzcache/table-variables/', + 'All modifications are single threaded, and selects have really low row estimates.') ; - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @minimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; -END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.no_stats_warning = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 35, + 100, + 'Statistics', + 'Columns With No Statistics', + 'https://www.brentozar.com/blitzcache/columns-no-statistics/', + 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.relop_warnings = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 36, + 100, + 'Warnings', + 'Operator Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', + 'Check the plan for more details.') ; -RAISERROR('Displaying analysis of plan cache.', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 37, + 100, + 'Indexes', + 'Table Scans (Heaps)', + 'https://www.brentozar.com/archive/2012/05/video-heaps/', + 'This may not be a problem. Run sp_BlitzIndex for more information.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.backwards_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 38, + 200, + 'Indexes', + 'Backwards Scans', + 'https://www.brentozar.com/blitzcache/backwards-scans/', + 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; -DECLARE @columns NVARCHAR(MAX) = N'' ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_index = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 39, + 100, + 'Indexes', + 'Forced Indexes', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans, and will prevent missing index requests.') ; -IF @ExpertMode = 0 -BEGIN - RAISERROR(N'Returning ExpertMode = 0', 0, 1) WITH NOWAIT; - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], - ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Avg Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Avg Writes], - PercentWrites AS [Write Weight], - AverageReturnedRows AS [Average Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - COALESCE(SetOptions, '''') AS [SET Options] '; -END; -ELSE -BEGIN - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], ' + @nl; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_seek = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 40, + 100, + 'Indexes', + 'Forced Seeks', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - IF @ExpertMode = 2 /* Opserver */ - BEGIN - RAISERROR(N'Returning Expert Mode = 2', 0, 1) WITH NOWAIT; - SET @columns += N' - SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN '', 20'' ELSE '''' END + - CASE WHEN compile_timeout = 1 THEN '', 18'' ELSE '''' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN '', 19'' ELSE '''' END + - CASE WHEN busy_loops = 1 THEN '', 16'' ELSE '''' END + - CASE WHEN is_forced_plan = 1 THEN '', 3'' ELSE '''' END + - CASE WHEN is_forced_parameterized > 0 THEN '', 5'' ELSE '''' END + - CASE WHEN unparameterized_query = 1 THEN '', 23'' ELSE '''' END + - CASE WHEN missing_index_count > 0 THEN '', 10'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_cursor = 1 THEN '', 4'' ELSE '''' END + - CASE WHEN is_parallel = 1 THEN '', 6'' ELSE '''' END + - CASE WHEN near_parallel = 1 THEN '', 7'' ELSE '''' END + - CASE WHEN frequent_execution = 1 THEN '', 1'' ELSE '''' END + - CASE WHEN plan_warnings = 1 THEN '', 8'' ELSE '''' END + - CASE WHEN parameter_sniffing = 1 THEN '', 2'' ELSE '''' END + - CASE WHEN long_running = 1 THEN '', 9'' ELSE '''' END + - CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + - CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + - CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + - CASE WHEN plan_multiple_plans = 1 THEN '', 21'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + - CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + - CASE WHEN is_key_lookup_expensive = 1 THEN '', 26'' ELSE '''' END + - CASE WHEN is_remote_query_expensive = 1 THEN '', 28'' ELSE '''' END + - CASE WHEN trace_flags_session IS NOT NULL THEN '', 29'' ELSE '''' END + - CASE WHEN is_unused_grant = 1 THEN '', 30'' ELSE '''' END + - CASE WHEN function_count > 0 THEN '', 31'' ELSE '''' END + - CASE WHEN clr_function_count > 0 THEN '', 32'' ELSE '''' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN '', 33'' ELSE '''' END + - CASE WHEN is_table_variable = 1 THEN '', 34'' ELSE '''' END + - CASE WHEN no_stats_warning = 1 THEN '', 35'' ELSE '''' END + - CASE WHEN relop_warnings = 1 THEN '', 36'' ELSE '''' END + - CASE WHEN is_table_scan = 1 THEN '', 37'' ELSE '''' END + - CASE WHEN backwards_scan = 1 THEN '', 38'' ELSE '''' END + - CASE WHEN forced_index = 1 THEN '', 39'' ELSE '''' END + - CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '', 40'' ELSE '''' END + - CASE WHEN columnstore_row_mode = 1 THEN '', 41'' ELSE '''' END + - CASE WHEN is_computed_scalar = 1 THEN '', 42'' ELSE '''' END + - CASE WHEN is_sort_expensive = 1 THEN '', 43'' ELSE '''' END + - CASE WHEN is_computed_filter = 1 THEN '', 44'' ELSE '''' END + - CASE WHEN index_ops >= 5 THEN '', 45'' ELSE '''' END + - CASE WHEN is_row_level = 1 THEN '', 46'' ELSE '''' END + - CASE WHEN is_spatial = 1 THEN '', 47'' ELSE '''' END + - CASE WHEN index_dml = 1 THEN '', 48'' ELSE '''' END + - CASE WHEN table_dml = 1 THEN '', 49'' ELSE '''' END + - CASE WHEN long_running_low_cpu = 1 THEN '', 50'' ELSE '''' END + - CASE WHEN low_cost_high_cpu = 1 THEN '', 51'' ELSE '''' END + - CASE WHEN stale_stats = 1 THEN '', 52'' ELSE '''' END + - CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + - CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + - CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + - CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + - CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END - , 2, 200000) AS opserver_warning , ' + @nl ; - END; - - SET @columns += N' ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentExecutionsByType AS [% Executions (Type)], - PercentCPUByType AS [% CPU (Type)], - PercentDurationByType AS [% Duration (Type)], - PercentReadsByType AS [% Reads (Type)], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows AS [Total Rows], - AverageReturnedRows AS [Avg Rows], - MinReturnedRows AS [Min Rows], - MaxReturnedRows AS [Max Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - NumberOfPlans AS [# Plans], - NumberOfDistinctPlans AS [# Distinct Plans], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - CachedPlanSize AS [Cached Plan Size (KB)], - CompileTime AS [Compile Time (ms)], - CompileCPU AS [Compile CPU (ms)], - CompileMemory AS [Compile memory (KB)], - COALESCE(SetOptions, '''') AS [SET Options], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - [SQL Handle More Info], - QueryHash AS [Query Hash], - [Query Hash More Info], - QueryPlanHash AS [Query Plan Hash], - StatementStartOffset, - StatementEndOffset, - [Remove Plan Handle From Cache], - [Remove SQL Handle From Cache]'; -END; - - - -SET @sql = N' -SELECT TOP (@Top) ' + @columns + @nl + N' -FROM ##bou_BlitzCacheProcs -WHERE SPID = @spid ' + @nl; - -IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount ' + @nl; - END; - -IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; - END; - -SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; -SET @sql += N' OPTION (RECOMPILE) ; '; - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - -EXEC sp_executesql @sql, N'@Top INT, @spid INT, @minimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; - -IF @HideSummary = 0 AND @ExportToExcel = 0 -BEGIN - IF @Reanalyze = 0 - BEGIN - RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 40, + 100, + 'Indexes', + 'Forced Scans', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - /* Build summary data */ - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE frequent_execution = 1 + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.columnstore_row_mode = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, - 1, + 41, 100, - 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', - 'Queries are being executed more than ' - + CAST (@execution_threshold AS VARCHAR(5)) - + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; + 'Indexes', + 'ColumnStore Row Mode', + 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', + 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE parameter_sniffing = 1 + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_computed_scalar = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, - 2, + 42, 50, - 'Parameterization', - 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', - 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; + 'Functions', + 'Computed Column UDF', + 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', + 'This can cause a whole mess of bad serializartion problems.') ; - /* Forced execution plans */ IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_plan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 3, - 5, - 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', - 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); + FROM ##BlitzCacheProcs p + WHERE p.is_sort_expensive = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 43, + 100, + 'Execution Plans', + 'Expensive Sort', + 'https://www.brentozar.com/blitzcache/expensive-sorts/', + 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); + FROM ##BlitzCacheProcs p + WHERE p.is_computed_filter = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 44, + 50, + 'Functions', + 'Filter UDF', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'Someone put a Scalar UDF in the WHERE clause!') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); + FROM ##BlitzCacheProcs p + WHERE p.index_ops >= 5 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 45, + 100, + 'Indexes', + '>= 5 Indexes Modified', + 'https://www.brentozar.com/blitzcache/many-indexes-modified/', + 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); + FROM ##BlitzCacheProcs p + WHERE p.is_row_level = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 46, + 200, + 'Complexity', + 'Row Level Security', + 'https://www.brentozar.com/blitzcache/row-level-security/', + 'You may see a lot of confusing junk in your query plan.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_parameterized = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 5, - 50, - 'Parameterization', - 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', - 'Execution plans have been compiled with forced parameterization.') ; + FROM ##BlitzCacheProcs p + WHERE p.is_spatial = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 47, + 200, + 'Complexity', + 'Spatial Index', + 'https://www.brentozar.com/blitzcache/spatial-indexes/', + 'Purely informational.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 6, - 200, - 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', - 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; + FROM ##BlitzCacheProcs p + WHERE p.index_dml = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 48, + 150, + 'Complexity', + 'Index DML', + 'https://www.brentozar.com/blitzcache/index-dml/', + 'This can cause recompiles and stuff.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE near_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 7, - 200, - 'Execution Plans', - 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', - 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; + FROM ##BlitzCacheProcs p + WHERE p.table_dml = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 49, + 150, + 'Complexity', + 'Table DML', + 'https://www.brentozar.com/blitzcache/table-dml/', + 'This can cause recompiles and stuff.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE plan_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 8, - 50, - 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; + FROM ##BlitzCacheProcs p + WHERE p.long_running_low_cpu = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 50, + 150, + 'Blocking', + 'Long Running Low CPU', + 'https://www.brentozar.com/blitzcache/long-running-low-cpu/', + 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE long_running = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 9, - 50, - 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', - 'Long running queries have been found. These are queries with an average duration longer than ' - + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) - + ' second(s). These queries should be investigated for additional tuning options.') ; + FROM ##BlitzCacheProcs p + WHERE p.low_cost_high_cpu = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 51, + 150, + 'Complexity', + 'Low Cost Query With High CPU', + 'https://www.brentozar.com/blitzcache/low-cost-high-cpu/', + 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.missing_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 10, - 50, - 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', - 'Queries found with missing indexes.'); + FROM ##BlitzCacheProcs p + WHERE p.stale_stats = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 52, + 150, + 'Statistics', + 'Statistics used have > 100k modifications in the last 7 days', + 'https://www.brentozar.com/blitzcache/stale-statistics/', + 'Ever heard of updating statistics?') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.downlevel_estimator = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 13, - 200, - 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', - 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); + FROM ##BlitzCacheProcs p + WHERE p.is_adaptive = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 53, + 200, + 'Complexity', + 'Adaptive joins', + 'https://www.brentozar.com/blitzcache/adaptive-joins/', + 'This join will sometimes do seeks, and sometimes do scans.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE implicit_conversions = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 14, - 50, - 'Performance', - 'Implicit Conversions', - 'http://brentozar.com/go/implicit', - 'One or more queries are comparing two fields that are not of the same data type.') ; + FROM ##BlitzCacheProcs p + WHERE p.is_spool_expensive = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 54, + 150, + 'Indexes', + 'Expensive Index Spool', + 'https://www.brentozar.com/blitzcache/eager-index-spools/', + 'Check operator predicates and output for index definition guidance') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE busy_loops = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 16, - 10, - 'Performance', - 'Frequently executed operators', - 'http://brentozar.com/blitzcache/busy-loops/', - 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); - + FROM ##BlitzCacheProcs p + WHERE p.is_spool_more_rows = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 55, + 150, + 'Indexes', + 'Large Index Row Spool', + 'https://www.brentozar.com/blitzcache/eager-index-spools/', + 'Check operator predicates and output for index definition guidance') ; + IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE tvf_join = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 17, - 50, - 'Performance', - 'Joining to table valued functions', - 'http://brentozar.com/blitzcache/tvf-join/', - 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); - + FROM ##BlitzCacheProcs p + WHERE p.is_bad_estimate = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 56, + 100, + 'Complexity', + 'Row Estimate Mismatch', + 'https://www.brentozar.com/blitzcache/bad-estimates/', + 'Estimated rows are different from average rows by a factor of 10000. This may indicate a performance problem if mismatches occur regularly') ; + IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_timeout = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 18, - 50, - 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', - 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); + FROM ##BlitzCacheProcs p + WHERE p.is_paul_white_electric = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 57, + 200, + 'Is Paul White Electric?', + 'This query has a Switch operator in it!', + 'https://www.sql.kiwi/2013/06/hello-operator-my-switch-is-bored.html', + 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_memory_limit_exceeded = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 19, - 50, - 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', - 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); + IF @v >= 14 OR (@v = 13 AND @build >= 5026) + BEGIN - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE warning_no_join_predicate = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 20, - 10, - 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', - 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + @@SPID, + 997, + 200, + 'Database Level Statistics', + 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], + 'https://www.brentozar.com/blitzcache/stale-statistics/' AS URL, + 'Consider updating statistics more frequently,' AS [Details] + FROM #stats_agg AS sa + GROUP BY sa.[Database] + HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) + AND AVG(sa.ModificationCount) >= 100000; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE plan_multiple_plans = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 21, - 200, - 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', - 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); + FROM ##BlitzCacheProcs p + WHERE p.is_row_goal = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 58, + 200, + 'Complexity', + 'Row Goals', + 'https://www.brentozar.com/go/rowgoals/', + 'This query had row goals introduced, which can be good or bad, and should be investigated for high read queries.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unmatched_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 22, - 100, - 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', - 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); + FROM ##BlitzCacheProcs p + WHERE p.is_big_spills = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 59, + 100, + 'TempDB', + '>500mb Spills', + 'https://www.brentozar.com/blitzcache/tempdb-spills/', + 'This query spills >500mb to tempdb on average. One way or another, this query didn''t get enough memory') ; - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unparameterized_query = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 23, - 100, - 'Parameterization', - 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', - 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); + + END; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_trivial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 24, - 100, - 'Execution Plans', - 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', - 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - + FROM ##BlitzCacheProcs p + WHERE p.is_mstvf = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 60, + 100, + 'Functions', + 'MSTVFs', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_forced_serial= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 25, - 10, - 'Execution Plans', - 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', - 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; + FROM ##BlitzCacheProcs p + WHERE p.is_mm_join = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 61, + 100, + 'Complexity', + 'Many to Many Merge', + 'https://www.brentozar.com/archive/2018/04/many-mysteries-merge-joins/', + 'These use secret worktables that could be doing lots of reads. Occurs when join inputs aren''t known to be unique. Can be really bad when parallel.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_key_lookup_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 26, - 100, - 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', - 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; + FROM ##BlitzCacheProcs p + WHERE p.is_nonsargable = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 62, + 50, + 'Non-SARGable queries', + 'non-SARGables', + 'https://www.brentozar.com/blitzcache/non-sargable-predicates/', + 'Looks for intrinsic functions and expressions as predicates, and leading wildcard LIKE searches.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_remote_query_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 28, - 100, - 'Execution Plans', - 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', - 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; + FROM ##BlitzCacheProcs p + WHERE CompileTime > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 63, + 100, + 'Complexity', + 'Long Compile Time', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries are taking >5 seconds to compile. This can be normal for large plans, but be careful if they compile frequently'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.trace_flags_session IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 29, - 100, - 'Trace Flags', - 'Session Level Trace Flags Enabled', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'Someone is enabling session level Trace Flags in a query.') ; + FROM ##BlitzCacheProcs p + WHERE CompileCPU > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 64, + 50, + 'Complexity', + 'High Compile CPU', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking >5 seconds of CPU to compile. If CPU is high and plans like this compile frequently, they may be related'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_unused_grant IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 30, - 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', - 'https://www.brentozar.com/blitzcache/unused-memory-grants/', - 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; + FROM ##BlitzCacheProcs p + WHERE CompileMemory > 1024 + AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 65, + 50, + 'Complexity', + 'High Compile Memory', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking 10% of Max Compile Memory. If you see high RESOURCE_SEMAPHORE_QUERY_COMPILE waits, these may be related'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 31, - 100, - 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; + FROM ##BlitzCacheProcs p + WHERE p.select_with_writes = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 66, + 50, + 'Complexity', + 'Selects w/ Writes', + 'https://dba.stackexchange.com/questions/191825/', + 'This is thrown when reads cause writes that are not already flagged as big spills (2016+) or index spools.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.clr_function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 32, - 100, - 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_expensive = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 67, + 150, + 'Expensive Table Spool', + 'You have a table spool, this is usually a sign that queries are doing unnecessary work', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join') ; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_more_rows = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 68, + 150, + 'Table Spools Many Rows', + 'You have a table spool that spools more rows than the query returns', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_variable = 1 + FROM #plan_creation p + WHERE (p.percent_24 > 0) AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 33, - 100, - 'Table Variables detected', - 'Beware nasty side effects', - 'https://www.brentozar.com/blitzcache/table-variables/', - 'All modifications are single threaded, and selects have really low row estimates.') ; + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT SPID, + 999, + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 'Plan Cache Instability' ELSE 'Plan Cache Stability' END AS Finding, + 'https://www.brentozar.com/archive/2018/07/tsql2sday-how-much-plan-cache-history-do-you-have/', + 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) + + ' total plans in your cache, with ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) + + '% plans created in the past 24 hours, ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) + + '% created in the past 4 hours, and ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) + + '% created in the past 1 hour. ' + + 'When these percentages are high, it may be a sign of memory pressure or plan cache instability.' + FROM #plan_creation p ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.no_stats_warning = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 35, - 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', - 'https://www.brentozar.com/blitzcache/columns-no-statistics/', - 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; + FROM #plan_usage p + WHERE p.percent_duplicate > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 'Many Duplicate Plans' ELSE 'Duplicate Plans' END AS Finding, + 'https://www.brentozar.com/archive/2018/03/why-multiple-plans-for-one-query-are-bad/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_duplicate) + + '% are duplicates with more than 5 entries' + + ', meaning similar queries are generating the same plan repeatedly.' + + ' Forced Parameterization may fix the issue. To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.relop_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 36, - 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Check the plan for more details.') ; + FROM #plan_usage p + WHERE p.percent_single > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 'Many Single-Use Plans' ELSE 'Single-Use Plans' END AS Finding, + 'https://www.brentozar.com/blitz/single-use-plans-procedure-cache/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_single) + + '% are single use plans' + + ', meaning SQL Server thinks it''s seeing a lot of "new" queries and creating plans for them.' + + ' Forced Parameterization and/or Optimize For Ad Hoc Workloads may fix the issue.' + + 'To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; + + IF @is_tokenstore_big = 1 + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT @@SPID, + 69, + 10, + N'Large USERSTORE_TOKENPERM cache: ' + CONVERT(NVARCHAR(11), @user_perm_gb_out) + N'GB', + N'The USERSTORE_TOKENPERM is taking up ' + CONVERT(NVARCHAR(11), @user_perm_percent) + + N'% of the buffer pool, and your plan cache seems to be unstable', + N'https://www.brentozar.com/go/userstore', + N'A growing USERSTORE_TOKENPERM cache can cause the plan cache to clear out' + IF @v >= 11 + BEGIN IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 37, - 100, - 'Table Scans', - 'Your database has HEAPs', - 'https://www.brentozar.com/archive/2012/05/video-heaps/', - 'This may not be a problem. Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.backwards_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 38, - 100, - 'Backwards Scans', - 'Indexes are being read backwards', - 'https://www.brentozar.com/blitzcache/backwards-scans/', - 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_index = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 39, - 100, - 'Index forcing', - 'Someone is using hints to force index usage', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans, and will prevent missing index requests.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_seek = 1 - OR p.forced_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 40, - 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.columnstore_row_mode = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 41, - 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', - 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', - 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_scalar = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 42, - 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', - 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', - 'This can cause a whole mess of bad serializartion problems.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_sort_expensive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 43, - 100, - 'Execution Plans', - 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', - 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_filter = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 44, - 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Someone put a Scalar UDF in the WHERE clause!') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_ops >= 5 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 45, - 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', - 'No URL yet', - 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_row_level = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', - 'No URL yet', - 'You may see a lot of confusing junk in your query plan.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spatial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 47, - 200, - 'Spatial Abuse', - 'You hit a Spatial Index', - 'No URL yet', - 'Purely informational.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 48, - 150, - 'Index DML', - 'Indexes were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.table_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 49, - 150, - 'Table DML', - 'Tables were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.long_running_low_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 50, - 150, - 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', - 'No URL yet', - 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.low_cost_high_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 51, - 150, - 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', - 'No URL yet', - 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.stale_stats = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 52, - 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', - 'No URL yet', - 'Ever heard of updating statistics?') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_adaptive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', - 'No URL yet', - 'Joe Sack rules.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_expensive = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 54, - 150, - 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_more_rows = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 55, - 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_bad_estimate = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 56, - 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', - 'No URL yet', - 'This may indicate a performance problem if mismatches occur regularly') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_paul_white_electric = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 998, - 200, - 'Is Paul White Electric?', - 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - - IF @v >= 14 - BEGIN - - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT - @@SPID, - 999, - 200, - 'Database Level Statistics', - 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], - '' AS URL, - 'Consider updating statistics more frequently,' AS [Details] - FROM #stats_agg AS sa - GROUP BY sa.[Database] - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000; - - - END; - - - IF EXISTS (SELECT 1/0 - FROM #plan_creation p - WHERE (p.percent_24 > 0) - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT SPID, - 999, - 254, - 'Plan Cache Information', - 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) - + ' total plans in your cache, with ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) - + '% plans created in the past 24 hours, ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) - + '% created in the past 4 hours, and ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) - + '% created in the past 1 hour.', - '', - 'If these percentages are high, it may be a sign of memory pressure or plan cache instability.' - FROM #plan_creation p ; - - IF @v >= 11 - BEGIN - IF EXISTS (SELECT 1/0 - FROM #trace_flags AS tf - WHERE tf.global_trace_flags IS NOT NULL - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + FROM #trace_flags AS tf + WHERE tf.global_trace_flags IS NOT NULL + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 1000, 255, @@ -17136,10 +19384,10 @@ BEGIN END; IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr + FROM ##BlitzCacheResults AS bcr WHERE bcr.Priority = 2147483646 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 2147483646, 255, @@ -17151,10 +19399,10 @@ BEGIN IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr + FROM ##BlitzCacheResults AS bcr WHERE bcr.Priority = 2147483647 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 2147483647, 255, @@ -17172,7 +19420,7 @@ BEGIN URL, Details, CheckID - FROM ##bou_BlitzCacheResults + FROM ##BlitzCacheResults WHERE SPID = @@SPID GROUP BY Priority, FindingsGroup, @@ -17180,30 +19428,30 @@ BEGIN URL, Details, CheckID - ORDER BY Priority ASC, CheckID ASC + ORDER BY Priority ASC, FindingsGroup, Finding, CheckID ASC OPTION (RECOMPILE); END; IF @Debug = 1 BEGIN - SELECT '##bou_BlitzCacheResults' AS table_name, * - FROM ##bou_BlitzCacheResults + SELECT '##BlitzCacheResults' AS table_name, * + FROM ##BlitzCacheResults OPTION ( RECOMPILE ); - SELECT '##bou_BlitzCacheProcs' AS table_name, * - FROM ##bou_BlitzCacheProcs + SELECT '##BlitzCacheProcs' AS table_name, * + FROM ##BlitzCacheProcs OPTION ( RECOMPILE ); - SELECT '#statements' AS table_name, * + SELECT '#statements' AS table_name, * FROM #statements AS s OPTION (RECOMPILE); - SELECT '#query_plan' AS table_name, * + SELECT '#query_plan' AS table_name, * FROM #query_plan AS qp OPTION (RECOMPILE); - SELECT '#relop' AS table_name, * + SELECT '#relop' AS table_name, * FROM #relop AS r OPTION (RECOMPILE); @@ -17246,7 +19494,27 @@ IF @Debug = 1 SELECT '#variable_info' AS table_name, * FROM #variable_info AS vi OPTION ( RECOMPILE ); - + + SELECT '#missing_index_xml' AS table_name, * + FROM #missing_index_xml AS mix + OPTION ( RECOMPILE ); + + SELECT '#missing_index_schema' AS table_name, * + FROM #missing_index_schema AS mis + OPTION ( RECOMPILE ); + + SELECT '#missing_index_usage' AS table_name, * + FROM #missing_index_usage AS miu + OPTION ( RECOMPILE ); + + SELECT '#missing_index_detail' AS table_name, * + FROM #missing_index_detail AS mid + OPTION ( RECOMPILE ); + + SELECT '#missing_index_pretty' AS table_name, * + FROM #missing_index_pretty AS mip + OPTION ( RECOMPILE ); + SELECT '#plan_creation' AS table_name, * FROM #plan_creation OPTION ( RECOMPILE ); @@ -17267,9 +19535,15 @@ IF @Debug = 1 FROM #trace_flags OPTION ( RECOMPILE ); - END; + SELECT '#plan_usage' AS table_name, * + FROM #plan_usage + OPTION ( RECOMPILE ); + END; + IF @OutputTableName IS NOT NULL + --Allow for output to ##DB so don't check for DB or schema name here + GOTO OutputResultsToTable; RETURN; --Avoid going into the AllSort GOTO /*Begin code to sort by all*/ @@ -17279,6 +19553,7 @@ RAISERROR('Beginning all sort loop', 0, 1) WITH NOWAIT; IF ( @Top > 10 + AND @SkipAnalysis = 0 AND @BringThePain = 0 ) BEGIN @@ -17321,67 +19596,55 @@ IF OBJECT_ID('tempdb.. #bou_allsort') IS NULL CREATE TABLE #bou_allsort ( Id INT IDENTITY(1, 1), - DatabaseName VARCHAR(128), + DatabaseName NVARCHAR(128), Cost FLOAT, QueryText NVARCHAR(MAX), - QueryType NVARCHAR(256), + QueryType NVARCHAR(258), Warnings VARCHAR(MAX), QueryPlan XML, missing_indexes XML, implicit_conversion_info XML, cached_execution_parameters XML, - ExecutionCount BIGINT, + ExecutionCount NVARCHAR(30), ExecutionsPerMinute MONEY, ExecutionWeight MONEY, - TotalCPU BIGINT, - AverageCPU BIGINT, + TotalCPU NVARCHAR(30), + AverageCPU NVARCHAR(30), CPUWeight MONEY, - TotalDuration BIGINT, - AverageDuration BIGINT, + TotalDuration NVARCHAR(30), + AverageDuration NVARCHAR(30), DurationWeight MONEY, - TotalReads BIGINT, - AverageReads BIGINT, + TotalReads NVARCHAR(30), + AverageReads NVARCHAR(30), ReadWeight MONEY, - TotalWrites BIGINT, - AverageWrites BIGINT, + TotalWrites NVARCHAR(30), + AverageWrites NVARCHAR(30), WriteWeight MONEY, AverageReturnedRows MONEY, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, + MinGrantKB NVARCHAR(30), + MaxGrantKB NVARCHAR(30), + MinUsedGrantKB NVARCHAR(30), + MaxUsedGrantKB NVARCHAR(30), AvgMaxMemoryGrant MONEY, + MinSpills NVARCHAR(30), + MaxSpills NVARCHAR(30), + TotalSpills NVARCHAR(30), + AvgSpills MONEY, PlanCreationTime DATETIME, LastExecutionTime DATETIME, + LastCompletionTime DATETIME, PlanHandle VARBINARY(64), SqlHandle VARBINARY(64), SetOptions VARCHAR(MAX), + QueryHash BINARY(8), + PlanGenerationNum NVARCHAR(30), + RemovePlanHandleFromCache NVARCHAR(200), Pattern NVARCHAR(20) ); END; -DECLARE @AllSortSql NVARCHAR(MAX) = N''; -DECLARE @MemGrant BIT; -SELECT @MemGrant = CASE WHEN ( - ( @v < 11 ) - OR ( - @v = 11 - AND @build < 6020 - ) - OR ( - @v = 12 - AND @build < 5000 - ) - OR ( - @v = 13 - AND @build < 1601 - ) - ) THEN 0 - ELSE 1 - END; - -IF LOWER(@SortOrder) = 'all' +IF @SortOrder = 'all' BEGIN RAISERROR('Beginning for ALL', 0, 1) WITH NOWAIT; SET @AllSortSql += N' @@ -17390,59 +19653,64 @@ SET @AllSortSql += N' INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); '; - IF @MemGrant = 0 + IF @VersionShowsMemoryGrants = 0 BEGIN IF @ExportToExcel = 1 BEGIN @@ -17454,29 +19722,24 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - IF @MemGrant = 1 + IF @VersionShowsMemoryGrants = 1 BEGIN SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; IF @ExportToExcel = 1 @@ -17489,84 +19752,142 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - -END; + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); -IF LOWER(@SortOrder) = 'all avg' -BEGIN -RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; -SET @AllSortSql += N' - DECLARE @ISH NVARCHAR(MAX) = N'''' - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; +END; + + +IF @SortOrder = 'all avg' +BEGIN +RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; +SET @AllSortSql += N' + DECLARE @ISH NVARCHAR(MAX) = N'''' + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); '; - IF @MemGrant = 0 + IF @VersionShowsMemoryGrants = 0 BEGIN IF @ExportToExcel = 1 BEGIN @@ -17578,29 +19899,24 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - IF @MemGrant = 1 + IF @VersionShowsMemoryGrants = 1 BEGIN SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; IF @ExportToExcel = 1 @@ -17613,18 +19929,72 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; END; IF @Debug = 1 @@ -17641,91 +20011,699 @@ END; PRINT SUBSTRING(@AllSortSql, 36000, 40000); END; - EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT', @i_DatabaseName = @DatabaseName, @i_Top = @Top; + EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT, @i_SkipAnalysis BIT, @i_OutputDatabaseName NVARCHAR(258), @i_OutputSchemaName NVARCHAR(258), @i_OutputTableName NVARCHAR(258), @i_CheckDateOverride DATETIMEOFFSET, @i_MinutesBack INT ', + @i_DatabaseName = @DatabaseName, @i_Top = @Top, @i_SkipAnalysis = @SkipAnalysis, @i_OutputDatabaseName = @OutputDatabaseName, @i_OutputSchemaName = @OutputSchemaName, @i_OutputTableName = @OutputTableName, @i_CheckDateOverride = @CheckDateOverride, @i_MinutesBack = @MinutesBack; +/* Avoid going into OutputResultsToTable + ... otherwise the last result (e.g. spills) would be recorded twice into the output table. +*/ +RETURN; /*End of AllSort section*/ + +/*Begin code to write results to table */ +OutputResultsToTable: + +RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; + +SELECT @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); + +/* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ +DECLARE @ValidOutputServer BIT; +DECLARE @ValidOutputLocation BIT; +DECLARE @LinkedServerDBCheck NVARCHAR(2000); +DECLARE @ValidLinkedServerDB INT; +DECLARE @tmpdbchk table (cnt int); +IF @OutputServerName IS NOT NULL + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; + + IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; +ELSE + BEGIN + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @StringToExecute NVARCHAR(MAX) = N'' ; + + IF @ValidOutputLocation = 1 + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + CONVERT + ( + nvarchar(MAX), + N'(ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,N'[',N''),N']',N'') + N'] PRIMARY KEY CLUSTERED(ID ASC));' + ); + + SET @StringToExecute += N'IF EXISTS(SELECT * FROM ' + +@OutputDatabaseName + +N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + +@OutputSchemaName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + +@OutputSchemaName + +N''' AND QUOTENAME(TABLE_NAME) = ''' + +@OutputTableName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.sys.computed_columns WHERE [name] = N''PlanCreationTimeHours'' AND QUOTENAME(OBJECT_NAME(object_id)) = N''' + +@OutputTableName + +N''' AND [definition] = N''(datediff(hour,[PlanCreationTime],sysdatetime()))'') +BEGIN + RAISERROR(''We noticed that you are running an old computed column definition for PlanCreationTimeHours, fixing that now'',0,0) WITH NOWAIT; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' DROP COLUMN [PlanCreationTimeHours]; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' ADD [PlanCreationTimeHours] AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]); +END '; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,'xml','nvarchar(max)'); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlySqlHandles = '''''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlyQueryHashes = '''''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''N/A''','''''N/A'''''); + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new LastCompletionTime column, add it. See Github #2377. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + ''')) AND name = ''LastCompletionTime'') + ALTER TABLE ' + @ObjectFullName + N' ADD LastCompletionTime DATETIME NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''LastCompletionTime''','''''LastCompletionTime'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new PlanGenerationNum column, add it. See Github #2514. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''PlanGenerationNum'') + ALTER TABLE ' + @ObjectFullName + N' ADD PlanGenerationNum BIGINT NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''PlanGenerationNum''','''''PlanGenerationNum'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new Pattern column, add it */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''Pattern'') + ALTER TABLE ' + @ObjectFullName + N' ADD Pattern NVARCHAR(20) NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''Pattern''','''''Pattern'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END + + IF @CheckDateOverride IS NULL + BEGIN + SET @CheckDateOverride = SYSDATETIMEOFFSET(); + END; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputServerName + '.' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputServerName + '.' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, CAST(QueryPlan AS NVARCHAR(MAX)), NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 1, 4000); + PRINT SUBSTRING(@StringToExecute, 4001, 4000); + PRINT SUBSTRING(@StringToExecute, 8001, 4000); + PRINT SUBSTRING(@StringToExecute, 12001, 4000); + PRINT SUBSTRING(@StringToExecute, 16001, 4000); + PRINT SUBSTRING(@StringToExecute, 20001, 4000); + PRINT SUBSTRING(@StringToExecute, 24001, 4000); + PRINT SUBSTRING(@StringToExecute, 28001, 4000); + PRINT SUBSTRING(@StringToExecute, 32001, 4000); + PRINT SUBSTRING(@StringToExecute, 36001, 4000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + ELSE + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + IF @ValidOutputServer = 1 + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF @OutputTableName IN ('##BlitzCacheProcs','##BlitzCacheResults') + BEGIN + RAISERROR('OutputTableName is a reserved name for this procedure. We only use ##BlitzCacheProcs and ##BlitzCacheResults, please choose another table name.', 16, 0); + END; + ELSE + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' + + 'CREATE TABLE ' + + @OutputTableName + + ' (ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,'[',''),']','') + '] PRIMARY KEY CLUSTERED(ID ASC));'; + SET @StringToExecute += N' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + + SET @StringToExecute += N' AND SPID = @@SPID '; + + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + PRINT SUBSTRING(@StringToExecute, 34000, 40000); + END; + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); +END; /* End of writing results to table */ + END; /*Final End*/ GO -IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;') +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 STATISTICS IO OFF; +SET STATISTICS TIME OFF; GO +IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzIndex AS RETURN 0;'); +GO -ALTER PROCEDURE [dbo].[sp_BlitzFirst] - @LogMessage NVARCHAR(4000) = NULL , - @Help TINYINT = 0 , - @AsOf DATETIMEOFFSET = NULL , - @ExpertMode TINYINT = 0 , - @Seconds INT = 5 , - @OutputType VARCHAR(20) = 'TABLE' , +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.*/ + @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 */ + /*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, + @IgnoreDatabases NVARCHAR(MAX) = NULL, /* Comma-delimited list of databases you want to skip */ + @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, + @OutputType VARCHAR(20) = 'TABLE' , @OutputServerName NVARCHAR(256) = NULL , @OutputDatabaseName NVARCHAR(256) = NULL , @OutputSchemaName NVARCHAR(256) = NULL , @OutputTableName NVARCHAR(256) = NULL , - @OutputTableNameFileStats NVARCHAR(256) = NULL , - @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , - @OutputTableNameWaitStats NVARCHAR(256) = NULL , - @OutputTableNameBlitzCache NVARCHAR(256) = NULL , - @OutputTableRetentionDays TINYINT = 7 , - @OutputXMLasNVARCHAR TINYINT = 0 , - @FilterPlansByDatabase VARCHAR(MAX) = NULL , - @CheckProcedureCache TINYINT = 0 , - @FileLatencyThresholdMS INT = 100 , - @SinceStartup TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0 , - @LogMessageCheckID INT = 38, - @LogMessagePriority TINYINT = 1, - @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', - @LogMessageFinding VARCHAR(200) = 'Logged from sp_BlitzFirst', - @LogMessageURL VARCHAR(200) = '', - @LogMessageCheckDate DATETIMEOFFSET = NULL, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT - WITH EXECUTE AS CALLER, RECOMPILE + @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 */, + @SortOrder NVARCHAR(50) = NULL, /* Only affects @Mode = 2. */ + @SortDirection NVARCHAR(4) = 'DESC', /* Only affects @Mode = 2. */ + @Help TINYINT = 0, + @Debug BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 +WITH RECOMPILE AS -BEGIN SET NOCOUNT ON; +SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); -IF @Help = 1 PRINT ' -sp_BlitzFirst from http://FirstResponderKit.org - -This script gives you a prioritized list of why your SQL Server is slow right now. +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; -This is not an overall health check - for that, check out sp_Blitz. +IF @Help = 1 +BEGIN +PRINT ' +/* +sp_BlitzIndex from http://FirstResponderKit.org + +This script analyzes the design and performance of your indexes. 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. Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. It - may work just fine on 2005, and if it does, hug your parents. Just don''t - file support issues if it breaks. - - If a temp table called #CustomPerfmonCounters exists for any other session, - but not our session, this stored proc will fail with an error saying the - temp table #CustomPerfmonCounters does not exist. - - @OutputServerName is not functional yet. - - If @OutputDatabaseName, SchemaName, TableName, etc are quoted with brackets, - the write to table may silently fail. Look, I never said I was good at this. + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. + -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important + for the user to understand if it is going to be offline and not just run a script. + -- Example 2: they do not include all the options the index may have been created with (padding, compression + filegroup/partition scheme etc.) + -- (The compression and filegroup index create syntax is not trivial because it is set at the partition + level and is not trivial to code.) + - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of problems.) Unknown limitations of this version: - - None. Like Zombo.com, the only limit is yourself. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + - We knew them once, but we forgot. MIT License -Copyright (c) 2017 Brent Ozar Unlimited +Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -17744,5588 +20722,4593 @@ 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. +'; +RETURN; +END; /* @Help = 1 */ -' +DECLARE @ScriptVersionName NVARCHAR(50); +DECLARE @DaysUptime NUMERIC(23,2); +DECLARE @DatabaseID INT; +DECLARE @ObjectID INT; +DECLARE @dsql NVARCHAR(MAX); +DECLARE @params NVARCHAR(MAX); +DECLARE @msg NVARCHAR(4000); +DECLARE @ErrorSeverity INT; +DECLARE @ErrorState INT; +DECLARE @Rowcount BIGINT; +DECLARE @SQLServerProductVersion NVARCHAR(128); +DECLARE @SQLServerEdition INT; +DECLARE @FilterMB INT; +DECLARE @collation NVARCHAR(256); +DECLARE @NumDatabases INT; +DECLARE @LineFeed NVARCHAR(5); +DECLARE @DaysUptimeInsertValue NVARCHAR(256); +DECLARE @DatabaseToIgnore NVARCHAR(MAX); +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 */ +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 */ -RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; -DECLARE @StringToExecute NVARCHAR(MAX), - @ParmDefinitions NVARCHAR(4000), - @Parm1 NVARCHAR(4000), - @OurSessionID INT, - @LineFeed NVARCHAR(10), - @StockWarningHeader NVARCHAR(500), - @StockWarningFooter NVARCHAR(100), - @StockDetailsHeader NVARCHAR(100), - @StockDetailsFooter NVARCHAR(100), - @StartSampleTime DATETIMEOFFSET, - @FinishSampleTime DATETIMEOFFSET, - @FinishSampleTimeWaitFor DATETIME, - @ServiceName sysname, - @OutputTableNameFileStats_View NVARCHAR(256), - @OutputTableNamePerfmonStats_View NVARCHAR(256), - @OutputTableNameWaitStats_View NVARCHAR(256), - @OutputTableNameWaitStats_Categories NVARCHAR(256), - @ObjectFullName NVARCHAR(2000), - @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', - @BlitzCacheMinutesBack INT, - @BlitzCacheSortOrder VARCHAR(50), - @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , - @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , - @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName ; -/* Sanitize our inputs */ -SELECT - @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), - @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), - @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), - @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); +/* Let's get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); +SET @SortDirection = LOWER(@SortDirection); + +SET @LineFeed = CHAR(13) + CHAR(10); +SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); +SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ +SET @FilterMB=250; +SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate); +SET @IgnoreDatabases = REPLACE(REPLACE(LTRIM(RTRIM(@IgnoreDatabases)), CHAR(10), ''), CHAR(13), ''); SELECT - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), - @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), - @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), - @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), - /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ - @LineFeed = CHAR(13) + CHAR(10), - @StartSampleTime = SYSDATETIMEOFFSET(), - @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), - @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()), - @OurSessionID = @@SPID; + @OptimizeForSequentialKey = + CASE WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.indexes') + AND ac.name = N'optimize_for_sequential_key' + ) + THEN 1 + ELSE 0 + END; -IF @LogMessage IS NOT NULL - BEGIN +RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; - RAISERROR('Saving LogMessage to table',10,1) WITH NOWAIT; + +IF(@OutputType NOT IN ('TABLE','NONE')) +BEGIN + RAISERROR('Invalid value for parameter @OutputType. Expected: (TABLE;NONE)',12,1); + RETURN; +END; - /* Try to set the output table parameters if they don't exist */ - IF @OutputSchemaName IS NULL AND @OutputTableName IS NULL AND @OutputDatabaseName IS NULL - BEGIN - SET @OutputSchemaName = N'[dbo]'; - SET @OutputTableName = N'[BlitzFirst]'; +IF(@UsualStatisticsSamplingPercent <= 0 OR @UsualStatisticsSamplingPercent > 100) +BEGIN + RAISERROR('Invalid value for parameter @UsualStatisticsSamplingPercent. Expected: 1 to 100',12,1); + RETURN; +END; - /* Look for the table in the current database */ - SELECT TOP 1 @OutputDatabaseName = QUOTENAME(TABLE_CATALOG) - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'BlitzFirst'; +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; + SET @OutputType = 'NONE' +END; + +IF(@OutputType = 'NONE') +BEGIN - IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') - SET @OutputDatabaseName = '[DBAtools]'; + IF ((@OutputServerName IS NOT NULL) AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) + BEGIN + RAISERROR('Parameter @OutputServerName is specified, rest of @Output* parameters needs to also be specified',12,1); + RETURN; + END; - END + IF(@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL) + BEGIN + RAISERROR('This procedure should be called with a value for @OutputTableName, @OutputSchemaName and @OutputDatabaseName parameters, as @OutputType is set to NONE',12,1); + RETURN; + END; + /* Output is supported for all modes, no reason to not bring pain and output + IF(@BringThePain = 1) + BEGIN + RAISERROR('Incompatible Parameters: @BringThePain set to 1 and @OutputType set to NONE',12,1); + RETURN; + END; + */ + /* Eventually limit by mode + IF(@Mode not in (0,4)) + BEGIN + RAISERROR('Incompatible Parameters: @Mode set to %d and @OutputType set to NONE',12,1,@Mode); + RETURN; + END; + */ +END; - IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL - OR NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; - RETURN; - END - IF @LogMessageCheckDate IS NULL - SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @LogMessageCheckDate, 121) + ''', @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)' +IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL + DROP TABLE #IndexSanity; - EXECUTE sp_executesql @StringToExecute, - N'@LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', - @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; +IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL + DROP TABLE #IndexPartitionSanity; - RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL + DROP TABLE #IndexSanitySize; - RETURN; - END +IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL + DROP TABLE #IndexColumns; -IF @SinceStartup = 1 - SELECT @Seconds = 0, @ExpertMode = 1; - -IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT @StartSampleTime = DATEADD(ms, AVG(-wait_time_ms), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.dm_os_wait_stats w - WHERE wait_type IN ('BROKER_TASK_STOP','DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', - 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT') -ELSE IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.databases - WHERE database_id = 2; -ELSE - SELECT @StartSampleTime = SYSDATETIMEOFFSET(), @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()); +IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL + DROP TABLE #MissingIndexes; -IF @OutputType = 'SCHEMA' -BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)' +IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL + DROP TABLE #ForeignKeys; -END -ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL -BEGIN - /* They want to look into the past. */ +IF OBJECT_ID('tempdb..#UnindexedForeignKeys') IS NOT NULL + DROP TABLE #UnindexedForeignKeys; - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT CheckDate, [Priority], [FindingsGroup], [Finding], [URL], CAST([Details] AS [XML]) AS Details,' - + '[HowToStopIt], [CheckID], [StartTime], [LoginName], [NTUserName], [OriginalLoginName], [ProgramName], [HostName], [DatabaseID],' - + '[DatabaseName], [OpenTransactionCount], [QueryPlan], [QueryText] FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE CheckDate >= DATEADD(mi, -15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' AND CheckDate <= DATEADD(mi, 15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; - EXEC(@StringToExecute); +IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL + DROP TABLE #BlitzIndexResults; + +IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL + DROP TABLE #IndexCreateTsql; +IF OBJECT_ID('tempdb..#DatabaseList') IS NOT NULL + DROP TABLE #DatabaseList; -END /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ -ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ -BEGIN - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - +IF OBJECT_ID('tempdb..#Statistics') IS NOT NULL + DROP TABLE #Statistics; - RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#PartitionCompressionInfo') IS NOT NULL + DROP TABLE #PartitionCompressionInfo; - /* - We start by creating #BlitzFirstResults. It's a temp table that will store - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. +IF OBJECT_ID('tempdb..#ComputedColumns') IS NOT NULL + DROP TABLE #ComputedColumns; + +IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL + DROP TABLE #TraceStatus; - #BlitzFirstResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can - download that from http://FirstResponderKit.org if you want to build - a tool that relies on the output of sp_BlitzFirst. - */ +IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL + DROP TABLE #TemporalTables; - IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL - DROP TABLE #BlitzFirstResults; - CREATE TABLE #BlitzFirstResults - ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt NVARCHAR(MAX) NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - QueryStatsNowID INT NULL, - QueryStatsFirstID INT NULL, - PlanHandle VARBINARY(64) NULL, - DetailsInt INT NULL, - ); +IF OBJECT_ID('tempdb..#CheckConstraints') IS NOT NULL + DROP TABLE #CheckConstraints; - IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL - DROP TABLE #WaitStats; - CREATE TABLE #WaitStats (Pass TINYINT NOT NULL, wait_type NVARCHAR(60), wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT, SampleTime DATETIMEOFFSET); +IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL + DROP TABLE #FilteredIndexes; - IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL - DROP TABLE #FileStats; - CREATE TABLE #FileStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - avg_stall_read_ms INT , - avg_stall_write_ms INT - ); +IF OBJECT_ID('tempdb..#Ignore_Databases') IS NOT NULL + DROP TABLE #Ignore_Databases; - IF OBJECT_ID('tempdb..#QueryStats') IS NOT NULL - DROP TABLE #QueryStats; - CREATE TABLE #QueryStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass INT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [sql_handle] VARBINARY(64), - statement_start_offset INT, - statement_end_offset INT, - plan_generation_num BIGINT, - plan_handle VARBINARY(64), - execution_count BIGINT, - total_worker_time BIGINT, - total_physical_reads BIGINT, - total_logical_writes BIGINT, - total_logical_reads BIGINT, - total_clr_time BIGINT, - total_elapsed_time BIGINT, - creation_time DATETIMEOFFSET, - query_hash BINARY(8), - query_plan_hash BINARY(8), - Points TINYINT - ); +IF OBJECT_ID('tempdb..#IndexResumableOperations') IS NOT NULL + DROP TABLE #IndexResumableOperations; - IF OBJECT_ID('tempdb..#PerfmonStats') IS NOT NULL - DROP TABLE #PerfmonStats; - CREATE TABLE #PerfmonStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL - ); +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 + DROP TABLE #dm_db_index_operational_stats - IF OBJECT_ID('tempdb..#PerfmonCounters') IS NOT NULL - DROP TABLE #PerfmonCounters; - CREATE TABLE #PerfmonCounters ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL - ); + RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; + CREATE TABLE #BlitzIndexResults + ( + blitz_result_id INT IDENTITY PRIMARY KEY, + check_id INT NOT NULL, + index_sanity_id INT NULL, + Priority INT NULL, + findings_group NVARCHAR(4000) NOT NULL, + finding NVARCHAR(200) NOT NULL, + [database_name] NVARCHAR(128) NULL, + URL NVARCHAR(200) NOT NULL, + details NVARCHAR(MAX) NOT NULL, + index_definition NVARCHAR(MAX) NOT NULL, + secret_columns NVARCHAR(MAX) NULL, + index_usage_summary NVARCHAR(MAX) NULL, + index_size_summary NVARCHAR(MAX) NULL, + create_tsql NVARCHAR(MAX) NULL, + more_info NVARCHAR(MAX) NULL, + sample_query_plan XML NULL + ); - IF OBJECT_ID('tempdb..#FilterPlansByDatabase') IS NOT NULL - DROP TABLE #FilterPlansByDatabase; - CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); + CREATE TABLE #IndexSanity + ( + [index_sanity_id] INT IDENTITY PRIMARY KEY CLUSTERED, + [database_id] SMALLINT NOT NULL , + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [index_type] TINYINT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [object_name] NVARCHAR(128) NOT NULL , + index_name NVARCHAR(128) NULL , + key_column_names NVARCHAR(MAX) NULL , + key_column_names_with_sort_order NVARCHAR(MAX) NULL , + key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , + count_key_columns INT NULL , + include_column_names NVARCHAR(MAX) NULL , + include_column_names_no_types NVARCHAR(MAX) NULL , + count_included_columns INT NULL , + partition_key_column_name NVARCHAR(MAX) NULL, + filter_definition NVARCHAR(MAX) NOT NULL , + optimize_for_sequential_key BIT NULL, + is_indexed_view BIT NOT NULL , + is_unique BIT NOT NULL , + is_primary_key BIT NOT NULL , + is_unique_constraint BIT NOT NULL , + is_XML bit NOT NULL, + is_spatial BIT NOT NULL, + is_NC_columnstore BIT NOT NULL, + is_CX_columnstore BIT NOT NULL, + is_in_memory_oltp BIT NOT NULL , + is_disabled BIT NOT NULL , + is_hypothetical BIT NOT NULL , + is_padded BIT NOT NULL , + fill_factor SMALLINT NOT NULL , + user_seeks BIGINT NOT NULL , + user_scans BIGINT NOT NULL , + user_lookups BIGINT NOT NULL , + user_updates BIGINT NULL , + last_user_seek DATETIME NULL , + last_user_scan DATETIME NULL , + last_user_lookup DATETIME NULL , + last_user_update DATETIME NULL , + is_referenced_by_foreign_key BIT DEFAULT(0), + secret_columns NVARCHAR(MAX) NULL, + count_secret_columns INT NULL, + create_date DATETIME NOT NULL, + modify_date DATETIME NOT NULL, + filter_columns_not_in_index NVARCHAR(MAX), + [db_schema_object_name] AS [schema_name] + N'.' + [object_name] , + [db_schema_object_indexid] AS [schema_name] + N'.' + [object_name] + + CASE WHEN [index_name] IS NOT NULL THEN N'.' + index_name + ELSE N'' + END + N' (' + CAST(index_id AS NVARCHAR(20)) + N')' , + first_key_column_name AS CASE WHEN count_key_columns > 1 + THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) + ELSE key_column_names + END , + index_definition AS + CASE WHEN partition_key_column_name IS NOT NULL + THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' + ELSE '' + END + + CASE index_id + WHEN 0 THEN N'[HEAP] ' + WHEN 1 THEN N'[CX] ' + ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN N'[VIEW] ' + ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' + 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_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] ' + ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 AND is_unique_constraint = 0 THEN N'[UNIQUE] ' + ELSE N'' END + CASE WHEN is_unique_constraint = 1 AND is_primary_key = 0 THEN N'[UNIQUE CONSTRAINT] ' + ELSE N'' END + CASE WHEN count_key_columns > 0 THEN + N'[' + CAST(count_key_columns AS NVARCHAR(10)) + N' KEY' + + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END + + N'] ' + LTRIM(key_column_names_with_sort_order) + ELSE N'' END + CASE WHEN count_included_columns > 0 THEN + N' [' + CAST(count_included_columns AS NVARCHAR(10)) + N' INCLUDE' + + + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END + + N'] ' + include_column_names + ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition + ELSE N'' END , + [total_reads] AS user_seeks + user_scans + user_lookups, + [reads_per_write] AS CAST(CASE WHEN user_updates > 0 + THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) + ELSE 0 END AS MONEY) , + [index_usage_summary] AS + CASE WHEN is_spatial = 1 THEN N'Not Tracked' + WHEN is_disabled = 1 THEN N'Disabled' + ELSE N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), N'.00', N'') + + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN + N' (' + + RTRIM( + CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), N'.00', N'') + N' seek ' ELSE N'' END + + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), N'.00', N'') + N' scan ' ELSE N'' END + + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), N'.00', N'') + N' lookup' ELSE N'' END + ) + + N') ' + ELSE N' ' + END + + N'Writes: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), N'.00', N'') + END /* First "end" is about is_spatial */, + [more_info] AS + CASE WHEN is_in_memory_oltp = 1 + THEN N'EXEC dbo.sp_BlitzInMemoryOLTP @dbName=' + QUOTENAME([database_name],N'''') + + N', @tableName=' + QUOTENAME([object_name],N'''') + N';' + ELSE N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') + + N', @SchemaName=' + QUOTENAME([schema_name],N'''') + N', @TableName=' + QUOTENAME([object_name],N'''') + N';' + END + ); + RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; + IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') + CREATE UNIQUE INDEX uq_database_id_object_id_index_id ON #IndexSanity (database_id, object_id, index_id); - IF OBJECT_ID('tempdb..##WaitCategories') IS NULL - BEGIN - /* We reuse this one by default rather than recreate it every time. */ - CREATE TABLE ##WaitCategories - ( - WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, - WaitCategory NVARCHAR(128) NOT NULL, - Ignorable BIT DEFAULT 0 - ); - END /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ - IF 504 <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) - BEGIN - TRUNCATE TABLE ##WaitCategories; - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_NETWORK_IO','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BACKUPIO','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_CONNECTION_RECEIVE_TASK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_DISPATCHER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_ENDPOINT_STATE_MUTEX','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_EVENTHANDLER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_FORWARDER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_INIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_MASTERSTART','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_RECEIVE_WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_REGISTERALLENDPOINTS','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SERVICE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_START','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_STOP','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SUBMIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TO_FLUSH','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_OBJECT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_TABLE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_WORK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_SEND','Mirroring',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_STATE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_TMDOWN_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_WAITFOR_OUTCOME','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_ENLIST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_PREPARE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_RECOVERY','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TM','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TRANSACTION_ENLISTMENT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCPNTSYNC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EE_PMOLOCK','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXCHANGE','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXTERNAL_SCRIPT_NETWORK_IOF','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_READ','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_WRITE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_METADATA_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_PROPERTYLIST_CACHE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_RESTART_CRAWL','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FULLTEXT GATHERER','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AG_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_CRITICAL_SECTION_ENTRY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_MANAGER_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_UNLOAD_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_BULK_LOCK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_QUEUE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CLUSAPI_CALL','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_COMPRESSED_CACHE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CONNECTIVITY_INFO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_VERSIONING_STATE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RECOVERY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RESTART','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_COMMAND','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_COMPLETION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_START_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER_FILTER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_MANAGER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_PREPROC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_GROUP_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_WAIT','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGPROGRESS_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_DEQUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_STARTUP_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_PARTNER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_READ_ALL_NETWORKS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_CONNECTION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_UNDO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_REPLICAINFO_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_CANCELLATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_FILE_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_LIMIT_BACKUPS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_SYNC_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_TIMEOUT_TASK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_WAIT_FOR_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNC_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNCHRONIZING_THROTTLE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC_PROCESSING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_THROTTLE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TIMER_TASK','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_DBRLIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_SESSION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_POOL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_QUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_XRF_STACK_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('INSTANCE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_QUEUE_LIMIT','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_RETRY','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_DT','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_EX','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_KP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_NL','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_SH','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_UP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LAZYWRITER_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_PMM_LOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_RESERVE_APPEND','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_GRANT_UPDATE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MGR_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSSEARCH','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('NET_WAITFOR_PACKET','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ONDEMAND_TASK_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_DT','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_EX','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_KP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_NL','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_SH','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_UP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_DT','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_EX','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_KP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPVDIDEVICE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COCREATEINSTANCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COGETCLASSOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_CREATEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_DELETEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETCOMMANDTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETNEXTROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETRESULT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETROWSBYBOOKMARK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBFLUSH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBREADAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBWRITEAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_QUERYINTERFACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASESESSION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RESTARTPOSITION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREADANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETDATAFAILURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CONSOLEWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CREATEPARAM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DEBUG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSADDLINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKEXISTCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKHEALTHCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVELINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVEROOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTFOLDERCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTSHARECHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORTREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_BEGINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_COMMITREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_PREPAREREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FILESIZEGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_STARTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_GETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_EVENT_WAIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_REQUEST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_LOCKMONITOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_MSS_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ODBCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLE_UNINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETDATASOURCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETLITERALINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETSCHEMALOCK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_JOINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_SETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDBOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHENTICATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHORIZATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_BACKUPREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLOSEHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLUSTEROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMPLETEAUTHTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COPYFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTACQUIRECONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTIMPORTKEY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DECRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEIOCONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DIRSVC_NETWORKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DISCONNECTNAMEDPIPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DOMAINSERVICESOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DSGETDCNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DTCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ENCRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FILEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FINDFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FORMATMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREELIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GENERICOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETADDRINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETDISKFREESPACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILEATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETLONGPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETPROCADDRESS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMEPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LIBRARYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOADLIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOGONUSER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOOKUPACCOUNTSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MESSAGEQUEUEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MOVEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETGROUPGETUSERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETLOCALGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERMODALSGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_OPENDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PDH_WMI_INIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PIPEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PROCESSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYREGISTRY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REMOVEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REPORTEVENT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REVERTTOSELF','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_RSFXDEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SECURITYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SERVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETENDOFFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEPOINTER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEVALIDDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETNAMEDSECURITYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQLCLROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQMLAUNCH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYSIGNATURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYTRUST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VSSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WAITFORSINGLEOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WINSOCKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILEGATHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WSASETLASTERROR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_REENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_RESIZELOG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDREDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SB_STOPENDPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SERVER_STARTUP','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SHAREDMEM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SNIOPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSHOST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSTESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STARTRM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_CHECKPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_RECOVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STRESSDRIVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TRANSIMPORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETFINALIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TIMERRUN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XETESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ACTION_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CLUSTER_INTEGRATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_FAILOVER_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_JOIN','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_OFFLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_POST_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_SERVER_READY_CONNECTIONS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_WORKITEM_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADRSIM','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_ASYNC_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_SHUTDOWN_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QUERY_TRACEOUT','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REDO_THREAD_PENDING_WORK','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_CACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_HISTORYCACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_SCHEMA_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANFSINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANHASHTABLE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANTEXTINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPLICA_WRITES','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REQUEST_FOR_DEADLOCK_SEARCH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESERVED_MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE_QUERY_COMPILE','Compilation',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BPOOL_FLUSH','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BUFFERPOOL_HELPLW','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DCOMSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERDBREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERMDREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERUPGRADED','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MEMORYPOOL_ALLOCATEPAGES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MSDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_RETRY_VIRTUALALLOC','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_SYSTEMTASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_DEADLOCK_DETECTION','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_QUANTUM_PUNISHMENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_BUFFER_FLUSH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_BUFFER','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_READ_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_WRITE_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_PENDING_BUFFER_WRITERS','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_SHUTDOWN','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_WAIT_ENTRIES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('THREADPOOL','Worker Thread',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACE_EVTNOTIF','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACEWRITE','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_DT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_EX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_KP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_NL','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_SH','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_UP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRANSACTION_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('UCS_SESSION_REGISTRATION','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_FOR_RESULTS','User Wait',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITE_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITELOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_OWN_TRANSACTION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_RECLAIM_SESSION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTLOCKINFO','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTWORKSPACE_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); - END /* IF SELECT SUM(1) FROM ##WaitCategories <> 504 */ - - - - IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL - DROP TABLE #MasterFiles; - CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); - /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;' - ELSE - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;' - EXEC(@StringToExecute); - - IF @FilterPlansByDatabase IS NOT NULL - BEGIN - IF UPPER(LEFT(@FilterPlansByDatabase,4)) = 'USER' - BEGIN - INSERT INTO #FilterPlansByDatabase (DatabaseID) - SELECT database_id - FROM sys.databases - WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb') - END - ELSE - BEGIN - SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' - ;WITH a AS - ( - SELECT CAST(1 AS BIGINT) f, CHARINDEX(',', @FilterPlansByDatabase) t, 1 SEQ - UNION ALL - SELECT t + 1, CHARINDEX(',', @FilterPlansByDatabase, t + 1), SEQ + 1 - FROM a - WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 - ) - INSERT #FilterPlansByDatabase (DatabaseID) - SELECT SUBSTRING(@FilterPlansByDatabase, f, t - f) - FROM a - WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL - OPTION (MAXRECURSION 0) - END - END - - - SET @StockWarningHeader = '', - @StockDetailsHeader = ''; - - /* Get the instance name to use as a Perfmon counter prefix. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) - FROM sys.dm_os_performance_counters; - ELSE - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;' - EXEC(@StringToExecute); - SELECT @ServiceName = object_name FROM #PerfmonStats; - DELETE #PerfmonStats; - END - - /* Build a list of queries that were run in the last 10 seconds. - We're looking for the death-by-a-thousand-small-cuts scenario - where a query is constantly running, and it doesn't have that - big of an impact individually, but it has a ton of impact - overall. We're going to build this list, and then after we - finish our @Seconds sample, we'll compare our plan cache to - this list to see what ran the most. */ - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @CheckProcedureCache = 1 - BEGIN - RAISERROR('@CheckProcedureCache = 1, capturing first pass of plan cache',10,1) WITH NOWAIT; - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - EXEC(@StringToExecute); - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - END /*IF @CheckProcedureCache = 1 */ - - - IF EXISTS (SELECT * - FROM tempdb.sys.all_objects obj - INNER JOIN tempdb.sys.all_columns col1 ON obj.object_id = col1.object_id AND col1.name = 'object_name' - INNER JOIN tempdb.sys.all_columns col2 ON obj.object_id = col2.object_id AND col2.name = 'counter_name' - INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' - WHERE obj.name LIKE '%CustomPerfmonCounters%') - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters' - EXEC(@StringToExecute); - END - ELSE - BEGIN - /* Add our default Perfmon counters */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL) - /* Below counters added by Jefferson Elias */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL) - /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. - And yes, they actually hard-coded the version numbers into the counters. - For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group - */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL) - END - - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. - After we finish doing our checks, we'll take another sample and compare them. */ - RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - owt.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - 0 AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - os.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_read_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_reads END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_read] END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_write_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_writes END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_written] END , - mf.physical_name, - mf.type_desc - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), CASE @Seconds WHEN 0 THEN 0 ELSE dmv.cntr_value END, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; - - - /* Maintenance Tasks Running - Backup Running - CheckID 1 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 1 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Backup Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/backups/' AS URL, - 'Backup 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)) + '. ' - + CASE WHEN COALESCE(s.nt_user_name, s.login_name) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_user_name, s.login_name) + ' ') ELSE '' END AS Details, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'BACKUP%' - AND r.start_time <= DATEADD(minute, -5, GETDATE()); - - /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ - IF @Seconds > 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - BEGIN - SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; - EXEC(@StringToExecute); - END - - - /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ - IF @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%') - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 2 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'DBCC CHECK* Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/dbcc/' AS URL, - 'Corruption check 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) 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, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id - FROM sys.dm_tran_locks l - INNER JOIN sys.databases d ON l.resource_database_id = d.database_id - WHERE l.resource_type = N'DATABASE' - AND l.request_mode = N'S' - AND l.request_status = N'GRANT' - AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t - WHERE r.command LIKE 'DBCC%' - AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%'; - - - /* Maintenance Tasks Running - Restore Running - CheckID 3 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 3 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Restore Running' AS Finding, - 'http://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, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'RESTORE%'; - - - /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 4 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Database File Growing' AS Finding, - 'http://www.BrentOzar.com/go/instant' AS URL, - 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, - 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out http://www.BrentOzar.com/go/instant for details.' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - NULL AS DatabaseID, - NULL AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks t - INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id - INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' - - - /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ - IF @@VERSION NOT LIKE '%Azure%' AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) - BEGIN - SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 5 AS CheckID, - 1 AS Priority, - ''Query Problems'' AS FindingGroup, - ''Long-Running Query Blocking Others'' AS Finding, - ''http://www.BrentOzar.com/go/blocking'' AS URL, - ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows:'' ' - + @LineFeed + @LineFeed + - '+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, - ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, - (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, - COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id)) AS QueryText, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - r.[database_id] AS DatabaseID, - DB_NAME(r.database_id) AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks tBlocked - INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id - LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000;' - EXECUTE sp_executesql @StringToExecute; - END - - /* Query Problems - Plan Cache Erased Recently */ - IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT TOP 1 7 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Plan Cache Erased Recently' AS Finding, - 'http://www.BrentOzar.com/askbrent/plan-cache-erased-recently/' AS URL, - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed - + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed - + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed - + 'SQL Server has to use a lot of CPU power in order to build execution' + @LineFeed - + 'plans and put them in cache again. This causes high CPU loads.' AS Details, - 'Find who did that, and stop them from doing it again.' AS HowToStopIt - FROM sys.dm_exec_query_stats - ORDER BY creation_time - END; - - - /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) - SELECT 8 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Sleeping Query with Open Transactions' AS Finding, - 'http://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, - 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.[host_name] + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_request_end_time AS NVARCHAR(100)) + '. ' AS Details, - 'KILL ' + CAST(s.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - s.last_request_start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, - sessions_with_transactions.open_transaction_count AS OpenTransactionCount - FROM (SELECT session_id, SUM(open_transaction_count) AS open_transaction_count FROM sys.dm_exec_requests WHERE open_transaction_count > 0 GROUP BY session_id) AS sessions_with_transactions - INNER JOIN sys.dm_exec_sessions s ON sessions_with_transactions.session_id = s.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE s.status = 'sleeping' - AND s.last_request_end_time < DATEADD(ss, -10, SYSDATETIME()) - AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.session_id - AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')) - - - /* Query Problems - Query Rolling Back - CheckID 9 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText) - SELECT 9 AS CheckID, - 1 AS Priority, - 'Query Problems' AS FindingGroup, - 'Query Rolling Back' AS Finding, - 'http://www.BrentOzar.com/askbrent/rollback/' AS URL, - 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, - 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText - FROM sys.dm_exec_sessions s - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - LEFT OUTER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE r.status = 'rollback' - - - /* Server Performance - Page Life Expectancy Low - CheckID 10 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 10 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Page Life Expectancy Low' AS Finding, - 'http://www.BrentOzar.com/askbrent/page-life-expectancy/' AS URL, - 'SQL Server Buffer Manager:Page life expectancy is ' + CAST(c.cntr_value AS NVARCHAR(10)) + ' seconds.' + @LineFeed - + 'This means SQL Server can only keep data pages in memory for that many seconds after reading those pages in from storage.' + @LineFeed - + 'This is a symptom, not a cause - it indicates very read-intensive queries that need an index, or insufficient server memory.' AS Details, - 'Add more memory to the server, or find the queries reading a lot of data, and make them more efficient (or fix them with indexes).' AS HowToStopIt - FROM sys.dm_os_performance_counters c - WHERE object_name LIKE 'SQLServer:Buffer Manager%' - AND counter_name LIKE 'Page life expectancy%' - AND cntr_value < 300 - - /* Server Performance - Too Much Free Memory - CheckID 34 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 34 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Too Much Free Memory' AS Finding, - 'https://BrentOzar.com/go/freememory' AS URL, - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, - 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 35 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Target Memory Lower Than Max' AS Finding, - 'https://BrentOzar.com/go/target' AS URL, - N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed - + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, - 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt - FROM sys.configurations cMax - INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' - AND cTarget.counter_name = N'Target Server Memory (KB) ' - WHERE cMax.name = 'max server memory (MB)' - AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) - AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ - AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ - - /* Server Info - Database Size, Total GB - CheckID 21 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 21 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Size, Total GB' AS Finding, - CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, - SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM #MasterFiles - WHERE database_id > 4 - - /* Server Info - Database Count - CheckID 22 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 22 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Count' AS Finding, - CAST(SUM(1) AS VARCHAR(100)) AS Details, - SUM (1) AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM sys.databases - WHERE database_id > 4 - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds < 30 - BEGIN - /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - /* Highlight if non SQL processes are using >25% CPU */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - ,record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLUsage - 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 - (y.SQLUsage + y.SystemIdle) >= 25 - - END /* IF @Seconds < 30 */ - - RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; - - - /* End of checks. If we haven't waited @Seconds seconds, wait. */ - IF SYSDATETIMEOFFSET() < @FinishSampleTime - BEGIN - RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; - WAITFOR TIME @FinishSampleTimeWaitFor; - END - - RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - owt.wait_type, - SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - os.wait_type, - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, - SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, - SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - vfs.io_stall_read_ms , - vfs.num_of_reads , - vfs.[num_of_bytes_read], - vfs.io_stall_write_ms , - vfs.num_of_writes , - vfs.[num_of_bytes_written], - mf.physical_name, - mf.type_desc, - 0, - 0 - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), dmv.cntr_value, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ - UPDATE fNow - SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms - WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0 - - UPDATE fNow - SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms - WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0 - - UPDATE pNow - SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, - [value_per_second] = ((1.0 * pNow.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime)) - FROM #PerfmonStats pNow - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pNow.[object_name] AND pFirst.counter_name = pNow.counter_name AND (pFirst.instance_name = pNow.instance_name OR (pFirst.instance_name IS NULL AND pNow.instance_name IS NULL)) - AND pNow.ID > pFirst.ID - WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; - - - /* If we're within 10 seconds of our projected finish time, do the plan cache analysis. */ - IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIME()) > 10 AND @CheckProcedureCache = 1 - BEGIN - - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'http://www.BrentOzar.com/go/topqueries', - 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.') - - END - ELSE IF @CheckProcedureCache = 1 - BEGIN - - - RAISERROR('@CheckProcedureCache = 1, capturing second pass of plan cache',10,1) WITH NOWAIT; - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - /* Old version pre-2016/06/13: - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - ELSE - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - */ - SET @ParmDefinitions = N'@StartSampleTimeText NVARCHAR(100)'; - SET @Parm1 = CONVERT(NVARCHAR(100), CAST(@StartSampleTime AS DATETIME), 127); - - EXECUTE sp_executesql @StringToExecute, @ParmDefinitions, @StartSampleTimeText = @Parm1; - - RAISERROR('@CheckProcedureCache = 1, totaling up plan cache metrics',10,1) WITH NOWAIT; - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT 0 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - - - RAISERROR('@CheckProcedureCache = 1, so analyzing execution plans',10,1) WITH NOWAIT; - /* - Pick the most resource-intensive queries to review. Update the Points field - in #QueryStats - if a query is in the top 10 for logical reads, CPU time, - duration, or execution, add 1 to its points. - */ - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_elapsed_time > qsFirst.total_elapsed_time - AND qsNow.Pass = 2 - AND qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - ORDER BY (qsNow.total_elapsed_time - COALESCE(qsFirst.total_elapsed_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_logical_reads > qsFirst.total_logical_reads - AND qsNow.Pass = 2 - AND qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - ORDER BY (qsNow.total_logical_reads - COALESCE(qsFirst.total_logical_reads, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_worker_time > qsFirst.total_worker_time - AND qsNow.Pass = 2 - AND qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */ - ORDER BY (qsNow.total_worker_time - COALESCE(qsFirst.total_worker_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.execution_count > qsFirst.execution_count - AND qsNow.Pass = 2 - AND (qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - OR qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - OR qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */) - ORDER BY (qsNow.execution_count - COALESCE(qsFirst.execution_count, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - /* Query Stats - CheckID 17 - Most Resource-Intensive Queries */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle) - SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'http://www.BrentOzar.com/go/topqueries', - 'Query stats during the sample:' + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes - (COALESCE(qsFirst.total_logical_writes, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time - (COALESCE(qsFirst.total_clr_time, 0)) AS NVARCHAR(100)) + @LineFeed + - @LineFeed + @LineFeed + 'Query stats since ' + CONVERT(NVARCHAR(100), qsNow.creation_time ,121) + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count AS NVARCHAR(100)) + - CASE qsTotal.execution_count WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time AS NVARCHAR(100)) + - CASE qsTotal.total_elapsed_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time AS NVARCHAR(100)) + - CASE qsTotal.total_worker_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads AS NVARCHAR(100)) + - CASE qsTotal.total_logical_reads WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes AS NVARCHAR(100)) + - CASE qsTotal.total_logical_writes WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_writes / qsTotal.total_logical_writes AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time AS NVARCHAR(100)) + - CASE qsTotal.total_clr_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_clr_time / qsTotal.total_clr_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - --@LineFeed + @LineFeed + 'Query hash: ' + CAST(qsNow.query_hash AS NVARCHAR(100)) + @LineFeed + - --@LineFeed + @LineFeed + 'Query plan hash: ' + CAST(qsNow.query_plan_hash AS NVARCHAR(100)) + - @LineFeed AS Details, - 'See the URL for tuning tips on why this query may be consuming resources.' AS HowToStopIt, - qp.query_plan, - QueryText = SUBSTRING(st.text, - (qsNow.statement_start_offset / 2) + 1, - ((CASE qsNow.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qsNow.statement_end_offset - END - qsNow.statement_start_offset) / 2) + 1), - qsNow.ID AS QueryStatsNowID, - qsFirst.ID AS QueryStatsFirstID, - qsNow.plan_handle AS PlanHandle - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp - WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL - - UPDATE #BlitzFirstResults - SET DatabaseID = CAST(attr.value AS INT), - DatabaseName = DB_NAME(CAST(attr.value AS INT)) - FROM #BlitzFirstResults - CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr - WHERE attr.attribute = 'dbid' - - - END /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ - - - RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; - - /* Wait Stats - CheckID 6 */ - /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT TOP 10 6 AS CheckID, - 200 AS Priority, - 'Wait Stats' AS FindingGroup, - wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ - N'http://www.brentozar.com/sql/wait-stats/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_time_ms > (wBase.wait_time_ms + (.5 * (DATEDIFF(ss,@StartSampleTime,@FinishSampleTime)) * 1000)) /* Only look for things we've actually waited on for half of the time or more */ - ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; - - /* Server Performance - Poison Wait Detected - CheckID 30 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT 30 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Poison Wait Detected: ' + wNow.wait_type AS Finding, - N'http://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') AND wNow.wait_time_ms > wBase.wait_time_ms; - - - /* Server Performance - Slow Data File Reads - CheckID 11 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 11 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Data File Reads' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these reads: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average read latency during the sample: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for data file read speed: 20ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > (fBase.io_stall_read_ms + 1000) - WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'ROWS' - ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; - END - - /* Server Performance - Slow Log File Writes - CheckID 12 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 12 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Log File Writes' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these writes: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average write latency during the sample: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for log file write speed: 3ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > (fBase.io_stall_write_ms + 1000) - WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'LOG' - ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; - END - - - /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 13 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Growing' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-growing/' AS URL, - 'Number of growths 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Growths' - AND value_delta > 0 - - - /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 14 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Shrinking' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-shrinking/' AS URL, - 'Number of shrinks 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Shrinks' - AND value_delta > 0 - - /* Query Problems - Compilations/Sec High - CheckID 15 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 15 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/compilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, - 'To find the queries that are compiling, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'If dynamic SQL or non-parameterized strings are involved, consider enabling Forced Parameterization. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Compilations are more than 10% of batch requests per second */ - - /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 16 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Re-Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/recompilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, - 'To find the queries that are being forced to recompile, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'Examine those plans to find out which objects are changing so quickly that they hit the stats update threshold. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Re-Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Recompilations are more than 10% of batch requests per second */ - - /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 29 AS CheckID, - 40 AS Priority, - 'Table Problems' AS FindingGroup, - 'Forwarded Fetches/Sec High' AS Finding, - 'https://BrentOzar.com/go/fetch/' AS URL, - CAST(ps.value_delta AS NVARCHAR(20)) + ' Forwarded Records (from SQLServer:Access Methods counter)' + @LineFeed - + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, - 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Access Methods' AND psComp.counter_name = 'Forwarded Records/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Access Methods' - AND ps.counter_name = 'Forwarded Records/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - - /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 31 AS CheckID, - 50 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Garbage Collection in Progress' AS Finding, - 'https://BrentOzar.com/go/garbage/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed - + 'This can happen due to memory pressure (causing In-Memory OLTP to shrink its footprint) or' + @LineFeed - + 'due to transactional workloads that constantly insert/delete data.' AS Details, - 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Garbage Collection' - AND ps.counter_name = 'Rows processed/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Transactions Aborted' AS Finding, - 'https://BrentOzar.com/go/aborted/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed - + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, - 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Transactions' AND psComp.counter_name = 'Transactions aborted/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Transactions' - AND ps.counter_name = 'Transactions aborted/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'Query Problems' AS FindingGroup, - 'Suboptimal Plans/Sec High' AS Finding, - 'https://BrentOzar.com/go/suboptimal/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed - + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, - 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Workload GroupStats' AND psComp.counter_name = 'Suboptimal plans/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Workload GroupStats' - AND ps.counter_name = 'Suboptimal plans/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - - - /* Server Info - Batch Requests per Sec - CheckID 19 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Batch Requests per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec'; - - - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - - /* Server Info - SQL Compilations/sec - CheckID 25 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 25 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Compilations/sec'; - - /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 26 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Re-Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Re-Compilations/sec'; - - /* Server Info - Wait Time per Core per Sec - CheckID 20 */ - IF @Seconds > 0 - BEGIN - WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), - waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), - cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Wait Time per Core per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST((waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS NVARCHAR(20)) AS Details, - (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS DetailsInt - FROM cores i - CROSS JOIN waits1 - CROSS JOIN waits2; - END - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds >= 30 - BEGIN - /* If we're waiting 30+ seconds, run this check at the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - END /* IF @Seconds < 30 */ - - - /* If we didn't find anything, apologize. */ - IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) - BEGIN - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 1 , - 'No Problems Found' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' - ); - - END /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - ); - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - VALUES ( -1 , - 0 , - 'sp_BlitzFirst ' + CAST(CONVERT(DATETIMEOFFSET, @VersionDate, 102) AS VARCHAR(100)), - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'We hope you found this tool useful.' - ); - - /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old */ - IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 0 AS Priority , - 'Outdated sp_BlitzFirst' AS FindingsGroup , - 'sp_BlitzFirst is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details - END - - RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; - - - /* If they want to run sp_BlitzCache and export to table, go for it. */ - IF @OutputTableNameBlitzCache IS NOT NULL - AND @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; - - /* Set the sp_BlitzCache sort order based on their top wait type */ - - /* First, check for poison waits - CheckID 30 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 30) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE_QUERY_COMPILE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: THREADPOOL' THEN 'executions' - WHEN Finding = 'Poison Wait Detected: LOG_RATE_GOVERNOR' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_CATCHUP_THROTTLE' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_COMMIT_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_ROLLBACK_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_SLOW_SECONDARY_THROTTLE' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 30 - ORDER BY DetailsInt DESC; - END - - /* Too much free memory - which probably indicates queries finished w/huge grants - CheckID 34 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 34) - SET @BlitzCacheSortOrder = 'memory grant'; - - /* Next, Compilations/Sec High - CheckID 15 and 16 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID IN (15,16)) - SET @BlitzCacheSortOrder = 'compilations'; - - /* Still not set? Use the top wait type. */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 6) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'ASYNC_NETWORK_IO' THEN 'duration' - WHEN Finding = 'CXPACKET' THEN 'reads' - WHEN Finding = 'LATCH_EX' THEN 'reads' - WHEN Finding LIKE 'LCK%' THEN 'duration' - WHEN Finding LIKE 'PAGEIOLATCH%' THEN 'reads' - WHEN Finding = 'SOS_SCHEDULER_YIELD' THEN 'cpu' - WHEN Finding = 'WRITELOG' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 6 - ORDER BY DetailsInt DESC; - END - /* Still null? Just use the default. */ - - - - /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ - IF EXISTS (SELECT * FROM sys.objects o - INNER JOIN sys.parameters pMB ON o.object_id = pMB.object_id AND pMB.name = '@MinutesBack' - INNER JOIN sys.parameters pCDO ON o.object_id = pCDO.object_id AND pCDO.name = '@CheckDateOverride' - WHERE o.name = 'sp_BlitzCache') - BEGIN - /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; - EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; - - /* If there's no data, let's just analyze the last 15 minutes of the plan cache */ - IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 - SET @BlitzCacheMinutesBack = 15; - - IF @BlitzCacheSortOrder IS NOT NULL - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @SortOrder = @BlitzCacheSortOrder, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - ELSE - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - - ELSE /* No sp_BlitzCache found, or it's outdated */ - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 36 AS CheckID , - 0 AS Priority , - 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , - 'Update Your sp_BlitzCache' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details - END - - RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; - - END /* End running sp_BlitzCache */ - - /* @OutputTableName lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND @OutputTableName NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - - EXEC(@StringToExecute); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + (CONVERT(NVARCHAR(100), @StartSampleTime, 121)) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NULL) CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - /* @OutputTableNameFileStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameFileStats IS NOT NULL - AND @OutputTableNameFileStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameFileStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats_View + ' AS ' + @LineFeed - + 'SELECT f.ServerName, f.CheckDate, f.DatabaseID, f.DatabaseName, f.FileID, f.FileLogicalName, f.TypeDesc, f.PhysicalName, f.SizeOnDiskMB' + @LineFeed - + ', DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth' + @LineFeed - + ', (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms' + @LineFeed - + ', io_stall_read_ms_average = CASE WHEN (f.num_of_reads - fPrior.num_of_reads) = 0 THEN 0 ELSE (f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads) END' + @LineFeed - + ', (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads' + @LineFeed - + ', (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read' + @LineFeed - + ', (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms' + @LineFeed - + ', io_stall_write_ms_average = CASE WHEN (f.num_of_writes - fPrior.num_of_writes) = 0 THEN 0 ELSE (f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes) END' + @LineFeed - + ', (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes' + @LineFeed - + ', (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName AND f.DatabaseID = fPrior.DatabaseID AND f.FileID = fPrior.FileID AND f.CheckDate > fPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fMiddle ON f.ServerName = fMiddle.ServerName AND f.DatabaseID = fMiddle.DatabaseID AND f.FileID = fMiddle.FileID AND f.CheckDate > fMiddle.CheckDate AND fMiddle.CheckDate > fPrior.CheckDate' + @LineFeed - + 'WHERE fMiddle.ID IS NULL AND f.num_of_reads >= fPrior.num_of_reads AND f.num_of_writes >= fPrior.num_of_writes - AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameFileStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNamePerfmonStats IS NOT NULL - AND @OutputTableNamePerfmonStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNamePerfmonStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed - + 'SELECT p.ServerName, p.CheckDate, p.object_name, p.counter_name, p.instance_name' + @LineFeed - + ', DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', p.cntr_value' + @LineFeed - + ', p.cntr_type' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) AS cntr_delta' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) * 1.0 / DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS cntr_delta_per_second' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' p' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pPrior ON p.ServerName = pPrior.ServerName AND p.object_name = pPrior.object_name AND p.counter_name = pPrior.counter_name AND p.instance_name = pPrior.instance_name AND p.CheckDate > pPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pMiddle ON p.ServerName = pMiddle.ServerName AND p.object_name = pMiddle.object_name AND p.counter_name = pMiddle.counter_name AND p.instance_name = pMiddle.instance_name AND p.CheckDate > pMiddle.CheckDate AND pMiddle.CheckDate > pPrior.CheckDate' + @LineFeed - + 'WHERE pMiddle.ID IS NULL AND DATEDIFF(MI, pPrior.CheckDate, p.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END; - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNamePerfmonStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNameWaitStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameWaitStats IS NOT NULL - AND @OutputTableNameWaitStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameWaitStats + ''') ' + @LineFeed - + 'BEGIN' + @LineFeed - + 'CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID));' + @LineFeed - + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed - + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed - + 'END' - - EXEC(@StringToExecute); - - /* Create the wait stats category table */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_Categories; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')' - EXEC(@StringToExecute); - END - - /* Make sure the wait stats category table has the current number of rows */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''IF (SELECT COALESCE(SUM(1),0) FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ') <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories)' + @LineFeed - + 'BEGIN ' + @LineFeed - + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed - + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed - + 'END'')' - EXEC(@StringToExecute); - - - /* Create the wait stats view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed - + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed - + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 60000.0 AS wait_time_minutes_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed - + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed - + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND w.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wMiddle ON w.ServerName = wMiddle.ServerName AND w.wait_type = wMiddle.wait_type AND w.CheckDate > wMiddle.CheckDate AND wMiddle.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed - + 'WHERE wMiddle.ID IS NULL AND w.wait_time_ms >= wPrior.wait_time_ms AND DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END + CREATE TABLE #IndexPartitionSanity + ( + [index_partition_sanity_id] INT IDENTITY, + [index_sanity_id] INT NULL , + [database_id] INT NOT NULL , + [object_id] INT NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL, + [index_id] INT NOT NULL , + [partition_number] INT NOT NULL , + row_count BIGINT NOT NULL , + reserved_MB NUMERIC(29,2) NOT NULL , + reserved_LOB_MB NUMERIC(29,2) NOT NULL , + reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + reserved_dictionary_MB NUMERIC(29,2) NOT NULL , + leaf_insert_count BIGINT NULL , + leaf_delete_count BIGINT NULL , + leaf_update_count BIGINT NULL , + range_scan_count BIGINT NULL , + singleton_lookup_count BIGINT NULL , + forwarded_fetch_count BIGINT NULL , + lob_fetch_in_pages BIGINT NULL , + lob_fetch_in_bytes BIGINT NULL , + row_overflow_fetch_in_pages BIGINT NULL , + row_overflow_fetch_in_bytes BIGINT NULL , + row_lock_count BIGINT NULL , + row_lock_wait_count BIGINT NULL , + row_lock_wait_in_ms BIGINT NULL , + page_lock_count BIGINT NULL , + page_lock_wait_count BIGINT NULL , + page_lock_wait_in_ms BIGINT NULL , + index_lock_promotion_attempt_count BIGINT NULL , + index_lock_promotion_count BIGINT NULL, + data_compression_desc NVARCHAR(60) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL + ); + CREATE TABLE #IndexSanitySize + ( + [index_sanity_size_id] INT IDENTITY NOT NULL , + [index_sanity_id] INT NULL , + [database_id] INT NOT NULL, + [schema_name] NVARCHAR(128) NOT NULL, + partition_count INT NOT NULL , + total_rows BIGINT NOT NULL , + total_reserved_MB NUMERIC(29,2) NOT NULL , + total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , + total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + total_reserved_dictionary_MB NUMERIC(29,2) NOT NULL , + total_leaf_delete_count BIGINT NULL, + total_leaf_update_count BIGINT NULL, + total_range_scan_count BIGINT NULL, + total_singleton_lookup_count BIGINT NULL, + total_forwarded_fetch_count BIGINT NULL, + total_row_lock_count BIGINT NULL , + total_row_lock_wait_count BIGINT NULL , + total_row_lock_wait_in_ms BIGINT NULL , + avg_row_lock_wait_in_ms BIGINT NULL , + total_page_lock_count BIGINT NULL , + total_page_lock_wait_count BIGINT NULL , + total_page_lock_wait_in_ms BIGINT NULL , + avg_page_lock_wait_in_ms BIGINT NULL , + total_index_lock_promotion_attempt_count BIGINT NULL , + total_index_lock_promotion_count BIGINT NULL , + data_compression_desc NVARCHAR(4000) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL, + index_size_summary AS ISNULL( + CASE WHEN partition_count > 1 + THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' + ELSE N'' + END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS MONEY), 1), N'.00', N'') + N' rows; ' + + CASE WHEN total_reserved_MB > 1024 THEN + CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' + ELSE + CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' + END + + CASE WHEN total_reserved_LOB_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END + WHEN total_reserved_LOB_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END + ELSE '' + END + + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' + WHEN total_reserved_row_overflow_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' + ELSE '' + END + + CASE WHEN total_reserved_dictionary_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Dictionaries' + WHEN total_reserved_dictionary_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Dictionaries' + ELSE '' + END , + N'Error- NULL in computed column'), + index_op_stats AS ISNULL( + ( + REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' + + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN + REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' + ELSE N'' END - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); + /* rows will only be in this dmv when data is in memory for the table */ + ), N'Table metadata not in memory'), + index_lock_wait_summary AS ISNULL( + CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND + total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits; ' + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN N'Lock escalation DISABLE.' + ELSE N'' + END + ELSE + CASE WHEN total_row_lock_wait_count > 0 THEN + N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_page_lock_wait_count > 0 THEN + N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS MONEY), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN + N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') + + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') +N'; ' + ELSE N'' + END + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN + N'Lock escalation is disabled.' + ELSE N'' + END + END + ,'Error- NULL in computed column') + ); - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); + CREATE TABLE #IndexColumns + ( + [database_id] INT NOT NULL, + [schema_name] NVARCHAR(128), + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [key_ordinal] INT NULL , + is_included_column BIT NULL , + is_descending_key BIT NULL , + [partition_ordinal] INT NULL , + column_name NVARCHAR(256) NOT NULL , + system_type_name NVARCHAR(256) NOT NULL, + max_length SMALLINT NOT NULL, + [precision] TINYINT NOT NULL, + [scale] TINYINT NOT NULL, + collation_name NVARCHAR(256) NULL, + is_nullable BIT NULL, + is_identity BIT NULL, + is_computed BIT NULL, + is_replicated BIT NULL, + is_sparse BIT NULL, + is_filestream BIT NULL, + seed_value DECIMAL(38,0) NULL, + increment_value DECIMAL(38,0) NULL , + last_value DECIMAL(38,0) NULL, + is_not_for_replication BIT NULL + ); + CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns + (database_id, object_id, index_id); - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameWaitStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END + CREATE TABLE #MissingIndexes + ([database_id] INT NOT NULL, + [object_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [table_name] NVARCHAR(128), + [statement] NVARCHAR(512) NOT NULL, + magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), + avg_total_user_cost NUMERIC(29,4) NOT NULL, + avg_user_impact NUMERIC(29,1) NOT NULL, + user_seeks BIGINT NOT NULL, + user_scans BIGINT NOT NULL, + unique_compiles BIGINT NULL, + equality_columns NVARCHAR(MAX), + equality_columns_with_data_type NVARCHAR(MAX), + inequality_columns NVARCHAR(MAX), + inequality_columns_with_data_type NVARCHAR(MAX), + included_columns NVARCHAR(MAX), + included_columns_with_data_type NVARCHAR(MAX), + is_low BIT, + [index_estimated_impact] AS + REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (user_seeks + user_scans) + AS BIGINT) AS MONEY), 1), '.00', '') + N' use' + + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END + +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) + + N'%; Avg query cost: ' + + CAST(avg_total_user_cost AS NVARCHAR(30)), + [missing_index_details] AS + CASE WHEN COALESCE(equality_columns_with_data_type,equality_columns) IS NOT NULL + THEN N'EQUALITY: ' + COALESCE(CAST(equality_columns_with_data_type AS NVARCHAR(MAX)), CAST(equality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(inequality_columns_with_data_type,inequality_columns) IS NOT NULL + THEN N'INEQUALITY: ' + COALESCE(CAST(inequality_columns_with_data_type AS NVARCHAR(MAX)), CAST(inequality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(included_columns_with_data_type,included_columns) IS NOT NULL + THEN N'INCLUDE: ' + COALESCE(CAST(included_columns_with_data_type AS NVARCHAR(MAX)), CAST(included_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END, + [create_tsql] AS N'CREATE INDEX [' + + LEFT(REPLACE(REPLACE(REPLACE(REPLACE( + ISNULL(equality_columns,N'')+ + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END + + ISNULL(inequality_columns,''),',','') + ,'[',''),']',''),' ','_') + + CASE WHEN included_columns IS NOT NULL THEN N'_Includes' ELSE N'' END, 128) + N'] ON ' + + [statement] + N' (' + ISNULL(equality_columns,N'') + + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END + + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + + ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END + + N' WITH (' + + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' + + N')' + + N';' + , + [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + + N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';', + [sample_query_plan] XML NULL + ); + CREATE TABLE #ForeignKeys ( + [database_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + foreign_key_name NVARCHAR(256), + parent_object_id INT, + parent_object_name NVARCHAR(256), + referenced_object_id INT, + referenced_object_name NVARCHAR(256), + is_disabled BIT, + is_not_trusted BIT, + is_not_for_replication BIT, + parent_fk_columns NVARCHAR(MAX), + referenced_fk_columns NVARCHAR(MAX), + update_referential_action_desc NVARCHAR(16), + delete_referential_action_desc NVARCHAR(60) + ); + CREATE TABLE #UnindexedForeignKeys + ( + [database_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + foreign_key_name NVARCHAR(256), + parent_object_name NVARCHAR(256), + parent_object_id INT, + referenced_object_name NVARCHAR(256), + referenced_object_id INT + ); + + CREATE TABLE #IndexCreateTsql ( + index_sanity_id INT NOT NULL, + create_tsql NVARCHAR(MAX) NOT NULL + ); + CREATE TABLE #DatabaseList ( + DatabaseName NVARCHAR(256), + secondary_role_allow_connections_desc NVARCHAR(50) - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; + ); - IF @OutputType = 'COUNT' AND @SinceStartup = 0 - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzFirstResults - END - ELSE - IF @OutputType = 'Opserver1' AND @SinceStartup = 0 - BEGIN + CREATE TABLE #PartitionCompressionInfo ( + [index_sanity_id] INT NULL, + [partition_compression_detail] NVARCHAR(4000) NULL + ); - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - r.[Details], - r.[HowToStopIt] , - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; - END - ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 - BEGIN + 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, + column_names NVARCHAR(MAX) NULL, + statistics_name NVARCHAR(128) NULL, + last_statistics_update DATETIME NULL, + days_since_last_stats_update INT NULL, + rows BIGINT NULL, + rows_sampled BIGINT NULL, + percent_sampled DECIMAL(18, 1) NULL, + histogram_steps INT NULL, + modification_counter BIGINT NULL, + percent_modifications DECIMAL(18, 1) 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, + persisted_sample_percent FLOAT NULL, + is_incremental BIT NULL + ); - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzFirstResults - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - Details; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - [QueryText], - [QueryPlan] - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS NVARCHAR(MAX)) AS Details, - CAST([HowToStopIt] AS NVARCHAR(MAX)) AS HowToStopIt, - CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 1 - BEGIN - IF @SinceStartup = 0 - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - CAST(@StockDetailsHeader + r.[Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + r.HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; + CREATE TABLE #ComputedColumns + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + table_name NVARCHAR(128) NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + column_name NVARCHAR(128) NULL, + is_nullable BIT NULL, + definition NVARCHAR(MAX) NULL, + uses_database_collation BIT NOT NULL, + is_persisted BIT NOT NULL, + is_computed BIT NOT NULL, + is_function INT NOT NULL, + column_definition NVARCHAR(MAX) NULL + ); + + CREATE TABLE #TraceStatus + ( + TraceFlag NVARCHAR(10) , + status BIT , + Global BIT , + Session BIT + ); - ------------------------- - --What happened: #WaitStats - ------------------------- - IF @Seconds = 0 - BEGIN - /* Measure waits in hours */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - CAST(c.[Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / 60 / 60 / 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; - END - ELSE + CREATE TABLE #TemporalTables + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NOT NULL, + database_id INT NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + table_name NVARCHAR(128) NOT NULL, + history_table_name NVARCHAR(128) 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, + history_table_object_id INT NULL + ); + + CREATE TABLE #CheckConstraints + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + table_name NVARCHAR(128) NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + constraint_name NVARCHAR(128) NULL, + is_disabled BIT NULL, + definition NVARCHAR(MAX) NULL, + uses_database_collation BIT NOT NULL, + is_not_trusted BIT NOT NULL, + is_function INT NOT NULL, + column_definition NVARCHAR(MAX) NULL + ); + + CREATE TABLE #FilteredIndexes + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + table_name NVARCHAR(128) NOT NULL, + index_name NVARCHAR(128) 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), + Reason NVARCHAR(100) + ); + +/* Sanitize our inputs */ +SELECT + @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); + + +IF @GetAllDatabases = 1 + BEGIN + INSERT INTO #DatabaseList (DatabaseName) + SELECT DB_NAME(database_id) + FROM sys.databases + WHERE user_access_desc = 'MULTI_USER' + AND state_desc = 'ONLINE' + AND database_id > 4 + AND DB_NAME(database_id) NOT LIKE 'ReportServer%' + AND DB_NAME(database_id) NOT LIKE 'rdsadmin%' + AND LOWER(name) NOT IN('dbatools', 'dbadmin', 'dbmaintenance') + AND is_distributor = 0 + OPTION ( RECOMPILE ); + + /* Skip non-readable databases in an AG - see Github issue #1160 */ + IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') + BEGIN + SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( + SELECT DB_NAME(d.database_id) + FROM sys.dm_hadr_availability_replica_states rs + INNER JOIN sys.databases d ON rs.replica_id = d.replica_id + INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id + WHERE rs.role_desc = ''SECONDARY'' + AND r.secondary_role_allow_connections_desc = ''NO'') + OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql; + + IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') BEGIN - /* Measure waits in seconds */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - c.[Wait Time (Seconds)], - 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 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, + 0, + N'Skipped non-readable AG secondary databases.', + N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', + N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', + 'http://FirstResponderKit.org', '', '', '', '' + ); END; + END; - ------------------------- - --What happened: #FileStats - ------------------------- - WITH readstats AS ( - SELECT 'PHYSICAL READS' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_reads - wd1.num_of_reads ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_reads - wd1.num_of_reads > 0 - THEN CAST(( wd2.bytes_read - wd1.bytes_read)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_read_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ), - writestats AS ( - SELECT - 'PHYSICAL WRITES' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_write_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_writes - wd1.num_of_writes ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_writes - wd1.num_of_writes > 0 - THEN CAST(( wd2.bytes_written - wd1.bytes_written)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_write_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ) - SELECT - Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM readstats - WHERE StallRank <=5 AND [MB Read/Written] > 0 - UNION ALL - SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM writestats - WHERE StallRank <=5 AND [MB Read/Written] > 0; + IF @IgnoreDatabases IS NOT NULL + AND LEN(@IgnoreDatabases) > 0 + BEGIN + RAISERROR(N'Setting up filter to ignore databases', 0, 1) WITH NOWAIT; + SET @DatabaseToIgnore = ''; + + WHILE LEN(@IgnoreDatabases) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreDatabases) > 0 + BEGIN + SET @DatabaseToIgnore = SUBSTRING(@IgnoreDatabases, 0, PATINDEX('%,%',@IgnoreDatabases)) ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + + SET @IgnoreDatabases = SUBSTRING(@IgnoreDatabases, LEN(@DatabaseToIgnore + ',') + 1, LEN(@IgnoreDatabases)) ; + END; + ELSE + BEGIN + SET @DatabaseToIgnore = @IgnoreDatabases ; + SET @IgnoreDatabases = NULL ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + END; + END; + + END + + END; +ELSE + BEGIN + INSERT INTO #DatabaseList + ( DatabaseName ) + SELECT CASE + WHEN @DatabaseName IS NULL OR @DatabaseName = N'' + THEN DB_NAME() + ELSE @DatabaseName END; + END; + +SET @NumDatabases = (SELECT COUNT(*) FROM #DatabaseList AS D LEFT OUTER JOIN #Ignore_Databases AS I ON D.DatabaseName = I.DatabaseName WHERE I.DatabaseName IS NULL AND ISNULL(D.secondary_role_allow_connections_desc, 'YES') != 'NO'); +SET @msg = N'Number of databases to examine: ' + CAST(@NumDatabases AS NVARCHAR(50)); +RAISERROR (@msg,0,1) WITH NOWAIT; + + + +/* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ + + +BEGIN TRY + IF @NumDatabases >= 50 AND @BringThePain != 1 AND @TableName IS NULL + BEGIN + + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, + 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16), GETDATE(), 121) END, + N'http://FirstResponderKit.org', + N'From Your Community Volunteers', + N'', + N'', + N'' + ); + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, + 0, + N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', + N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server and/or user.', + '', + 'http://FirstResponderKit.org', + N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', + '', + '', + '' + ); + + if(@OutputType <> 'NONE') + BEGIN + SELECT bir.blitz_result_id, + bir.check_id, + bir.index_sanity_id, + bir.Priority, + bir.findings_group, + bir.finding, + bir.database_name, + bir.URL, + bir.details, + bir.index_definition, + bir.secret_columns, + bir.index_usage_summary, + bir.index_size_summary, + bir.create_tsql, + bir.more_info + FROM #BlitzIndexResults AS bir; + RAISERROR('Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server', 12, 1); + END; + RETURN; - ------------------------- - --What happened: #PerfmonStats - ------------------------- + END; +END TRY +BEGIN CATCH + RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; - SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, - pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, - pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, - pLast.cntr_value - pFirst.cntr_value AS ValueDelta, - ((1.0 * pLast.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pLast.SampleTime)) AS ValuePerSecond - FROM #PerfmonStats pLast - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) - AND pLast.ID > pFirst.ID - WHERE pLast.cntr_value <> pFirst.cntr_value - ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name + SELECT @msg = ERROR_MESSAGE(), + @ErrorSeverity = ERROR_SEVERITY(), + @ErrorState = ERROR_STATE(); + RAISERROR (@msg, @ErrorSeverity, @ErrorState); + + WHILE @@trancount > 0 + ROLLBACK; - ------------------------- - --What happened: #QueryStats - ------------------------- - IF @CheckProcedureCache = 1 - BEGIN - - SELECT qsNow.*, qsFirst.* - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.Pass = 2 - END - ELSE - BEGIN - SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details] - END - END + RETURN; + END CATCH; - DROP TABLE #BlitzFirstResults; - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 -IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ +RAISERROR (N'Checking partition counts to exclude databases with over 100 partitions',0,1) WITH NOWAIT; +IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL + BEGIN + DECLARE partition_cursor CURSOR FOR + SELECT dl.DatabaseName + FROM #DatabaseList dl + LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName + WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL -END /* IF @LogMessage IS NULL */ -END /* ELSE IF @OutputType = 'SCHEMA' */ + OPEN partition_cursor + FETCH NEXT FROM partition_cursor INTO @DatabaseName + + WHILE @@FETCH_STATUS = 0 + BEGIN + /* Count the total number of partitions */ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; + IF @Rowcount > 100 + BEGIN + RAISERROR (N'Skipping database %s because > 100 partitions were found. To check this database, you must set @BringThePain = 1.',0,1,@DatabaseName) WITH NOWAIT; + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT @DatabaseName, 'Over 100 partitions found - use @BringThePain = 1 to analyze' + END; + FETCH NEXT FROM partition_cursor INTO @DatabaseName + END; + CLOSE partition_cursor + DEALLOCATE partition_cursor -SET NOCOUNT OFF; -GO + END; +INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) +SELECT 1, 0 , + 'Database Skipped', + i.DatabaseName, + 'http://FirstResponderKit.org', + i.Reason, '', '', '' +FROM #Ignore_Databases i; -/* How to run it: -EXEC dbo.sp_BlitzFirst +/* Last startup */ +IF COLUMNPROPERTY(OBJECT_ID('sys.dm_os_sys_info'),'sqlserver_start_time','ColumnID') IS NOT NULL +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, sqlserver_start_time, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.dm_os_sys_info; +END +ELSE +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.databases + WHERE database_id = 2; +END -With extra diagnostic info: -EXEC dbo.sp_BlitzFirst @ExpertMode = 1; +IF @DaysUptime = 0 OR @DaysUptime IS NULL + SET @DaysUptime = .01; -Saving output to tables: -EXEC sp_BlitzFirst -, @OutputDatabaseName = 'DBAtools' -, @OutputSchemaName = 'dbo' -, @OutputTableName = 'BlitzFirst' -, @OutputTableNameFileStats = 'BlitzFirst_FileStats' -, @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' -, @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' -, @OutputTableNameBlitzCache = 'BlitzCache' -*/ -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO +SELECT @DaysUptimeInsertValue = 'Server: ' + (CONVERT(VARCHAR(256), (SERVERPROPERTY('ServerName')))) + ' Days Uptime: ' + RTRIM(@DaysUptime); -IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzIndex AS RETURN 0;') -GO -ALTER PROCEDURE dbo.sp_BlitzIndex - @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.*/ - @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 */ - /*Note:@Filter doesn't do anything unless @Mode=0*/ - @SkipPartitions BIT = 0, - @SkipStatistics BIT = 1, - @GetAllDatabases BIT = 0, - @BringThePain BIT = 0, - @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @Help TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +/* Permission granted or unnecessary? Ok, let's go! */ -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; +RAISERROR (N'Starting loop through databases',0,1) WITH NOWAIT; +DECLARE c1 CURSOR +LOCAL FAST_FORWARD +FOR +SELECT dl.DatabaseName +FROM #DatabaseList dl +LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName +WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL +ORDER BY dl.DatabaseName; +OPEN c1; +FETCH NEXT FROM c1 INTO @DatabaseName; + WHILE @@FETCH_STATUS = 0 -IF @Help = 1 PRINT ' -/* -sp_BlitzIndex from http://FirstResponderKit.org - -This script analyzes the design and performance of your indexes. +BEGIN + + RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; + RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; + RAISERROR (@DatabaseName, 0, 1) WITH NOWAIT; -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. +SELECT @DatabaseID = [database_id] +FROM sys.databases + WHERE [name] = @DatabaseName + AND user_access_desc='MULTI_USER' + AND state_desc = 'ONLINE'; -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - The @OutputDatabaseName parameters are not functional yet. To check the - status of this enhancement request, visit: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/221 - - Does not analyze columnstore, spatial, XML, or full text indexes. If you - would like to contribute code to analyze those, head over to Github and - check out the issues list: http://FirstResponderKit.org - - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. - -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important - for the user to understand if it is going to be offline and not just run a script. - -- Example 2: they do not include all the options the index may have been created with (padding, compression - filegroup/partition scheme etc.) - -- (The compression and filegroup index create syntax is not trivial because it is set at the partition - level and is not trivial to code.) - - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of insanity.) +---------------------------------------- +--STEP 1: OBSERVE THE PATIENT +--This step puts index information into temp tables. +---------------------------------------- +BEGIN TRY + BEGIN + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'starting at %s',0,1, @d) WITH NOWAIT; -Unknown limitations of this version: - - We knew them once, but we forgot. + --Validate SQL Server Version -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/milestone/4?closed=1 + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 + )) <= 9 + BEGIN + SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2008 and higher. The version of this instance is: ' + @SQLServerProductVersion; + RAISERROR(@msg,16,1); + END; + --Short circuit here if database name does not exist. + IF @DatabaseName IS NULL OR @DatabaseID IS NULL + BEGIN + SET @msg='Database does not exist or is not online/multi-user: cannot proceed.'; + RAISERROR(@msg,16,1); + END; -MIT License + --Validate parameters. + IF (@Mode NOT IN (0,1,2,3,4)) + BEGIN + SET @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail, 4=diagnose detail'; + RAISERROR(@msg,16,1); + END; -Copyright (c) 2016 Brent Ozar Unlimited + IF (@Mode <> 0 AND @TableName IS NOT NULL) + BEGIN + SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; + RAISERROR(@msg,16,1); + END; -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: + IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) + BEGIN + SET @msg=N'@Filter only applies when @Mode=0 and @TableName is not specified. Please try again.'; + RAISERROR(@msg,16,1); + END; -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + IF (@SchemaName IS NOT NULL AND @TableName IS NULL) + BEGIN + SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.'; + RAISERROR(@msg,16,1); + END; -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. -' + IF (@TableName IS NOT NULL AND @SchemaName IS NULL) + BEGIN + SET @SchemaName=N'dbo'; + SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.'; + RAISERROR(@msg,1,1) WITH NOWAIT; + END; -DECLARE @ScriptVersionName NVARCHAR(50); -DECLARE @DaysUptime NUMERIC(23,2); -DECLARE @DatabaseID INT; -DECLARE @ObjectID INT; -DECLARE @dsql NVARCHAR(MAX); -DECLARE @params NVARCHAR(MAX); -DECLARE @msg NVARCHAR(4000); -DECLARE @ErrorSeverity INT; -DECLARE @ErrorState INT; -DECLARE @Rowcount BIGINT; -DECLARE @SQLServerProductVersion NVARCHAR(128); -DECLARE @SQLServerEdition INT; -DECLARE @FilterMB INT; -DECLARE @collation NVARCHAR(256); -DECLARE @NumDatabases INT; -DECLARE @LineFeed NVARCHAR(5); + --If a table is specified, grab the object id. + --Short circuit if it doesn't exist. + IF @TableName IS NOT NULL + BEGIN + SET @dsql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @ObjectID= OBJECT_ID + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on + so.schema_id=sc.schema_id + where so.type in (''U'', ''V'') + and so.name=' + QUOTENAME(@TableName,'''')+ N' + and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' + /*Has a row in sys.indexes. This lets us get indexed views.*/ + and exists ( + SELECT si.name + FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si + WHERE so.object_id=si.object_id) + OPTION (RECOMPILE);'; -SET @LineFeed = CHAR(13) + CHAR(10); -SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ -SET @FilterMB=250; -SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate) + SET @params='@ObjectID INT OUTPUT'; -RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); -IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL - DROP TABLE #IndexSanity; + EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; + + IF @ObjectID IS NULL + BEGIN + SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + + N'Please check your parameters.'; + RAISERROR(@msg,1,1); + RETURN; + END; + END; -IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL - DROP TABLE #IndexPartitionSanity; + --set @collation + SELECT @collation=collation_name + FROM sys.databases + WHERE database_id=@DatabaseID; -IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL - DROP TABLE #IndexSanitySize; + --insert columns for clustered indexes and heaps + --collect info on identity columns for this one + SET @dsql = N'/* sp_BlitzIndex */ + SET LOCK_TIMEOUT 1000; /* To fix locking bug in sys.identity_columns. See Github issue #2176. */ + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', + s.name, + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', + CAST(ic.seed_value AS DECIMAL(38,0)), + CAST(ic.increment_value AS DECIMAL(38,0)), + CAST(ic.last_value AS DECIMAL(38,0)), + ic.is_not_for_replication + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON + si.object_id=c.object_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON + c.object_id=ic.object_id and + c.column_id=ic.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + WHERE si.index_id in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N'OPTION (RECOMPILE);'; -IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL - DROP TABLE #IndexColumns; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); -IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL - DROP TABLE #MissingIndexes; + RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 1, 4000); + PRINT SUBSTRING(@dsql, 4001, 4000); + PRINT SUBSTRING(@dsql, 8001, 4000); + PRINT SUBSTRING(@dsql, 12001, 4000); + PRINT SUBSTRING(@dsql, 16001, 4000); + PRINT SUBSTRING(@dsql, 20001, 4000); + PRINT SUBSTRING(@dsql, 24001, 4000); + PRINT SUBSTRING(@dsql, 28001, 4000); + PRINT SUBSTRING(@dsql, 32001, 4000); + PRINT SUBSTRING(@dsql, 36001, 4000); + END; + BEGIN TRY + INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) + EXEC sp_executesql @dsql; + END TRY + BEGIN CATCH + RAISERROR (N'Failure inserting data into #IndexColumns for clustered indexes and heaps.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL + BEGIN + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END; -IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL - DROP TABLE #ForeignKeys; + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), + @ErrorSeverity = 0, @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; -IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL - DROP TABLE #BlitzIndexResults; - -IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL - DROP TABLE #IndexCreateTsql; + WHILE @@trancount > 0 + ROLLBACK; -IF OBJECT_ID('tempdb..#DatabaseList') IS NOT NULL - DROP TABLE #DatabaseList; + RETURN; + END CATCH; -IF OBJECT_ID('tempdb..#Statistics') IS NOT NULL - DROP TABLE #Statistics; -IF OBJECT_ID('tempdb..#PartitionCompressionInfo') IS NOT NULL - DROP TABLE #PartitionCompressionInfo; + --insert columns for nonclustered indexes + --this uses a full join to sys.index_columns + --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', + s.name, + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL AS is_sparse' END + N', + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL AS is_filestream' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON + si.object_id=c.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + WHERE si.index_id not in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N'OPTION (RECOMPILE);'; -IF OBJECT_ID('tempdb..#ComputedColumns') IS NOT NULL - DROP TABLE #ComputedColumns; - -IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); -IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL - DROP TABLE #TemporalTables; - - RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; - CREATE TABLE #BlitzIndexResults - ( - blitz_result_id INT IDENTITY PRIMARY KEY, - check_id INT NOT NULL, - index_sanity_id INT NULL, - Priority INT NULL, - findings_group VARCHAR(4000) NOT NULL, - finding VARCHAR(200) NOT NULL, - [database_name] VARCHAR(200) NULL, - URL VARCHAR(200) NOT NULL, - details NVARCHAR(4000) NOT NULL, - index_definition NVARCHAR(MAX) NOT NULL, - secret_columns NVARCHAR(MAX) NULL, - index_usage_summary NVARCHAR(MAX) NULL, - index_size_summary NVARCHAR(MAX) NULL, - create_tsql NVARCHAR(MAX) NULL, - more_info NVARCHAR(MAX)NULL - ); + RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream ) + EXEC sp_executesql @dsql; + + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + so.object_id, + si.index_id, + si.type, + @i_DatabaseName AS database_name, + COALESCE(sc.name, ''Unknown'') AS [schema_name], + COALESCE(so.name, ''Unknown'') AS [object_name], + COALESCE(si.name, ''Unknown'') AS [index_name], + CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, + si.is_unique, + si.is_primary_key, + si.is_unique_constraint, + CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, + 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.data_space_id = 0 then 1 else 0 end as is_in_memory_oltp, + si.is_disabled, + si.is_hypothetical, + si.is_padded, + si.fill_factor,' + + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N' + CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition + ELSE N'''' + END AS filter_definition' ELSE N''''' AS filter_definition' END + + CASE + WHEN @OptimizeForSequentialKey = 1 + THEN N', si.optimize_for_sequential_key' + ELSE N', CONVERT(BIT, NULL) AS optimize_for_sequential_key' + END + + N', + ISNULL(us.user_seeks, 0), + ISNULL(us.user_scans, 0), + ISNULL(us.user_lookups, 0), + ISNULL(us.user_updates, 0), + us.last_user_seek, + us.last_user_scan, + us.last_user_lookup, + us.last_user_update, + so.create_date, + so.modify_date + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si WITH (NOLOCK) + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc ON so.schema_id = sc.schema_id + 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 */ ' + + 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) + AND @TableName IS NULL ) + THEN N'AND ( us.user_seeks + us.user_scans + us.user_lookups + us.user_updates ) > 0' + ELSE N'' + END + + N'OPTION ( RECOMPILE ); + '; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); - CREATE TABLE #IndexSanity - ( - [index_sanity_id] INT IDENTITY PRIMARY KEY CLUSTERED, - [database_id] SMALLINT NOT NULL , - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [index_type] TINYINT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [object_name] NVARCHAR(128) NOT NULL , - index_name NVARCHAR(128) NULL , - key_column_names NVARCHAR(MAX) NULL , - key_column_names_with_sort_order NVARCHAR(MAX) NULL , - key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , - count_key_columns INT NULL , - include_column_names NVARCHAR(MAX) NULL , - include_column_names_no_types NVARCHAR(MAX) NULL , - count_included_columns INT NULL , - partition_key_column_name NVARCHAR(MAX) NULL, - filter_definition NVARCHAR(MAX) NOT NULL , - is_indexed_view BIT NOT NULL , - is_unique BIT NOT NULL , - is_primary_key BIT NOT NULL , - is_XML BIT NOT NULL, - is_spatial BIT NOT NULL, - is_NC_columnstore BIT NOT NULL, - is_CX_columnstore BIT NOT NULL, - is_disabled BIT NOT NULL , - is_hypothetical BIT NOT NULL , - is_padded BIT NOT NULL , - fill_factor SMALLINT NOT NULL , - user_seeks BIGINT NOT NULL , - user_scans BIGINT NOT NULL , - user_lookups BIGINT NOT NULL , - user_updates BIGINT NULL , - last_user_seek DATETIME NULL , - last_user_scan DATETIME NULL , - last_user_lookup DATETIME NULL , - last_user_update DATETIME NULL , - is_referenced_by_foreign_key BIT DEFAULT(0), - secret_columns NVARCHAR(MAX) NULL, - count_secret_columns INT NULL, - create_date DATETIME NOT NULL, - modify_date DATETIME NOT NULL, - [db_schema_object_name] AS [schema_name] + '.' + [object_name] , - [db_schema_object_indexid] AS [schema_name] + '.' + [object_name] - + CASE WHEN [index_name] IS NOT NULL THEN '.' + index_name - ELSE '' - END + ' (' + CAST(index_id AS NVARCHAR(20)) + ')' , - first_key_column_name AS CASE WHEN count_key_columns > 1 - THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) - ELSE key_column_names - END , - index_definition AS - CASE WHEN partition_key_column_name IS NOT NULL - THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' - ELSE '' - END + - CASE index_id - WHEN 0 THEN N'[HEAP] ' - WHEN 1 THEN N'[CX] ' - ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN '[VIEW] ' - ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' - 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_disabled = 1 THEN N'[DISABLED] ' - ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' - ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 THEN N'[UNIQUE] ' - ELSE N'' END + CASE WHEN count_key_columns > 0 THEN - N'[' + CAST(count_key_columns AS VARCHAR(10)) + N' KEY' - + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + LTRIM(key_column_names_with_sort_order) - ELSE N'' END + CASE WHEN count_included_columns > 0 THEN - N' [' + CAST(count_included_columns AS VARCHAR(10)) + N' INCLUDE' + - + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + include_column_names - ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition - ELSE N'' END , - [total_reads] AS user_seeks + user_scans + user_lookups, - [reads_per_write] AS CAST(CASE WHEN user_updates > 0 - THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) - ELSE 0 END AS MONEY) , - [index_usage_summary] AS N'Reads: ' + - REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), '.00', '') - + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN - N' (' - + RTRIM( - CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), '.00', '') + N' seek ' ELSE N'' END - + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), '.00', '') + N' scan ' ELSE N'' END - + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), '.00', '') + N' lookup' ELSE N'' END - ) - + N') ' - ELSE N' ' END - + N'Writes:' + - REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), '.00', ''), - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([object_name],'''') + N';' - ); - RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; - IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') - CREATE UNIQUE INDEX uq_database_id_object_id_index_id ON #IndexSanity (database_id, object_id, index_id); + RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + 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, + 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 ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; - CREATE TABLE #IndexPartitionSanity - ( - [index_partition_sanity_id] INT IDENTITY, - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL , - [object_id] INT NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL, - [index_id] INT NOT NULL , - [partition_number] INT NOT NULL , - row_count BIGINT NOT NULL , - reserved_MB NUMERIC(29,2) NOT NULL , - reserved_LOB_MB NUMERIC(29,2) NOT NULL , - reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - leaf_insert_count BIGINT NULL , - leaf_delete_count BIGINT NULL , - leaf_update_count BIGINT NULL , - range_scan_count BIGINT NULL , - singleton_lookup_count BIGINT NULL , - forwarded_fetch_count BIGINT NULL , - lob_fetch_in_pages BIGINT NULL , - lob_fetch_in_bytes BIGINT NULL , - row_overflow_fetch_in_pages BIGINT NULL , - row_overflow_fetch_in_bytes BIGINT NULL , - row_lock_count BIGINT NULL , - row_lock_wait_count BIGINT NULL , - row_lock_wait_in_ms BIGINT NULL , - page_lock_count BIGINT NULL , - page_lock_wait_count BIGINT NULL , - page_lock_wait_in_ms BIGINT NULL , - index_lock_promotion_attempt_count BIGINT NULL , - index_lock_promotion_count BIGINT NULL, - data_compression_desc VARCHAR(60) NULL - ); + RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; + IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL + BEGIN + /* Count the total number of partitions */ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; + IF @Rowcount > 100 + BEGIN + RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',0,1) WITH NOWAIT; + SET @SkipPartitions = 1; + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, 0 , + 'Some Checks Were Skipped', + '@SkipPartitions Forced to 1', + 'http://FirstResponderKit.org', CAST(@Rowcount AS NVARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' + ); + END; + END; - CREATE TABLE #IndexSanitySize - ( - [index_sanity_size_id] INT IDENTITY NOT NULL , - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128) NOT NULL, - partition_count INT NOT NULL , - total_rows BIGINT NOT NULL , - total_reserved_MB NUMERIC(29,2) NOT NULL , - total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , - total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - total_leaf_delete_count BIGINT NULL, - total_leaf_update_count BIGINT NULL, - total_range_scan_count BIGINT NULL, - total_singleton_lookup_count BIGINT NULL, - total_forwarded_fetch_count BIGINT NULL, - total_row_lock_count BIGINT NULL , - total_row_lock_wait_count BIGINT NULL , - total_row_lock_wait_in_ms BIGINT NULL , - avg_row_lock_wait_in_ms BIGINT NULL , - total_page_lock_count BIGINT NULL , - total_page_lock_wait_count BIGINT NULL , - total_page_lock_wait_in_ms BIGINT NULL , - avg_page_lock_wait_in_ms BIGINT NULL , - total_index_lock_promotion_attempt_count BIGINT NULL , - total_index_lock_promotion_count BIGINT NULL , - data_compression_desc VARCHAR(8000) NULL, - index_size_summary AS ISNULL( - CASE WHEN partition_count > 1 - THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' - ELSE N'' - END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS MONEY), 1), N'.00', N'') + N' rows; ' - + CASE WHEN total_reserved_MB > 1024 THEN - CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' - ELSE - CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' - END - + CASE WHEN total_reserved_LOB_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB LOB' - WHEN total_reserved_LOB_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB LOB' - ELSE '' - END - + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' - WHEN total_reserved_row_overflow_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' - ELSE '' - END , - N'Error- NULL in computed column'), - index_op_stats AS ISNULL( - ( - REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' - + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN - REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' - ELSE N'' END - /* rows will only be in this dmv when data is in memory for the table */ - ), N'Table metadata not in memory'), - index_lock_wait_summary AS ISNULL( - CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND - total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits.' - ELSE - CASE WHEN total_row_lock_wait_count > 0 THEN - N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_page_lock_wait_count > 0 THEN - N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN - N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') - + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') + N'.' - ELSE N'' - END - END - ,'Error- NULL in computed column') - ); - CREATE TABLE #IndexColumns - ( - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128), - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [key_ordinal] INT NULL , - is_included_column BIT NULL , - is_descending_key BIT NULL , - [partition_ordinal] INT NULL , - column_name NVARCHAR(256) NOT NULL , - system_type_name NVARCHAR(256) NOT NULL, - max_length SMALLINT NOT NULL, - [precision] TINYINT NOT NULL, - [scale] TINYINT NOT NULL, - collation_name NVARCHAR(256) NULL, - is_nullable BIT NULL, - is_identity BIT NULL, - is_computed BIT NULL, - is_replicated BIT NULL, - is_sparse BIT NULL, - is_filestream BIT NULL, - seed_value BIGINT NULL, - increment_value INT NULL , - last_value BIGINT NULL, - is_not_for_replication BIT NULL - ); - CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns - (database_id, object_id, index_id); + IF (@SkipPartitions = 0) + BEGIN + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 )) <= 2147483647 --Make change here + BEGIN + + RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - CREATE TABLE #MissingIndexes - ([database_id] INT NOT NULL, - [object_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [table_name] NVARCHAR(128), - [statement] NVARCHAR(512) NOT NULL, - magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), - avg_total_user_cost NUMERIC(29,4) NOT NULL, - avg_user_impact NUMERIC(29,1) NOT NULL, - user_seeks BIGINT NOT NULL, - user_scans BIGINT NOT NULL, - unique_compiles BIGINT NULL, - equality_columns NVARCHAR(4000), - inequality_columns NVARCHAR(4000), - included_columns NVARCHAR(4000), - is_low BIT, - [index_estimated_impact] AS - REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (user_seeks + user_scans) - AS BIGINT) AS MONEY), 1), '.00', '') + N' use' - + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END - +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) - + N'%; Avg query cost: ' - + CAST(avg_total_user_cost AS NVARCHAR(30)), - [missing_index_details] AS - CASE WHEN equality_columns IS NOT NULL THEN N'EQUALITY: ' + equality_columns + N' ' - ELSE N'' - END + CASE WHEN inequality_columns IS NOT NULL THEN N'INEQUALITY: ' + inequality_columns + N' ' - ELSE N'' - END + CASE WHEN included_columns IS NOT NULL THEN N'INCLUDES: ' + included_columns + N' ' - ELSE N'' - END, - [create_tsql] AS N'CREATE INDEX [ix_' + table_name + N'_' - + REPLACE(REPLACE(REPLACE(REPLACE( - ISNULL(equality_columns,N'')+ - CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END - + ISNULL(inequality_columns,''),',','') - ,'[',''),']',''),' ','_') - + CASE WHEN included_columns IS NOT NULL THEN N'_includes' ELSE N'' END + N'] ON ' - + [statement] + N' (' + ISNULL(equality_columns,N'') - + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END - + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + - ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END - + N' WITH (' - + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - + N';' - , - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';' - ); + --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 + --This change was made because on a table with lots of partitions, the OUTER APPLY was crazy slow. + + -- get relevant columns from sys.dm_db_partition_stats, sys.partitions and sys.objects + IF OBJECT_ID('tempdb..#dm_db_partition_stats_etc') IS NOT NULL + DROP TABLE #dm_db_partition_stats_etc; + + create table #dm_db_partition_stats_etc + ( + database_id smallint not null + , object_id int not null + , sname sysname NULL + , index_id int + , partition_number int + , partition_id bigint + , row_count bigint + , reserved_MB NUMERIC(29,2) + , reserved_LOB_MB NUMERIC(29,2) + , reserved_row_overflow_MB NUMERIC(29,2) + , lock_escalation_desc nvarchar(60) + , data_compression_desc nvarchar(60) + ) + + -- get relevant info from sys.dm_db_index_operational_stats + IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL + DROP TABLE #dm_db_index_operational_stats; + create table #dm_db_index_operational_stats + ( + database_id smallint not null + , object_id int not null + , index_id int + , partition_number int + , hobt_id bigint + , leaf_insert_count bigint + , leaf_delete_count bigint + , leaf_update_count bigint + , range_scan_count bigint + , singleton_lookup_count bigint + , forwarded_fetch_count bigint + , lob_fetch_in_pages bigint + , lob_fetch_in_bytes bigint + , row_overflow_fetch_in_pages bigint + , row_overflow_fetch_in_bytes bigint + , row_lock_count bigint + , row_lock_wait_count bigint + , row_lock_wait_in_ms bigint + , page_lock_count bigint + , page_lock_wait_count bigint + , page_lock_wait_in_ms bigint + , index_lock_promotion_attempt_count bigint + , index_lock_promotion_count bigint + , page_latch_wait_count bigint + , page_latch_wait_in_ms bigint + , page_io_latch_wait_count bigint + , page_io_latch_wait_in_ms bigint + ) + + SET @dsql = N' + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_partition_stats_etc at %s'',0,1, @d) WITH NOWAIT; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #dm_db_partition_stats_etc + ( + database_id, object_id, sname, index_id, partition_number, partition_id, row_count, reserved_MB, reserved_LOB_MB, reserved_row_overflow_MB, lock_escalation_desc, data_compression_desc + ) + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + ps.object_id, + s.name as sname, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' +'; + + SET @dsql = @dsql + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + N' + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; + + insert into #dm_db_index_operational_stats + ( + database_id + , object_id + , index_id + , partition_number + , hobt_id + , leaf_insert_count + , leaf_delete_count + , leaf_update_count + , range_scan_count + , singleton_lookup_count + , forwarded_fetch_count + , lob_fetch_in_pages + , lob_fetch_in_bytes + , row_overflow_fetch_in_pages + , row_overflow_fetch_in_bytes + , row_lock_count + , row_lock_wait_count + , row_lock_wait_in_ms + , page_lock_count + , page_lock_wait_count + , page_lock_wait_in_ms + , index_lock_promotion_attempt_count + , index_lock_promotion_count + , page_latch_wait_count + , page_latch_wait_in_ms + , page_io_latch_wait_count + , page_io_latch_wait_in_ms + ) + + select os.database_id + , os.object_id + , os.index_id + , os.partition_number ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', os.hobt_id ' + ELSE N', NULL AS hobt_id ' + END + N' + , os.leaf_insert_count + , os.leaf_delete_count + , os.leaf_update_count + , os.range_scan_count + , os.singleton_lookup_count + , os.forwarded_fetch_count + , os.lob_fetch_in_pages + , os.lob_fetch_in_bytes + , os.row_overflow_fetch_in_pages + , os.row_overflow_fetch_in_bytes + , os.row_lock_count + , os.row_lock_wait_count + , os.row_lock_wait_in_ms + , os.page_lock_count + , os.page_lock_wait_count + , os.page_lock_wait_in_ms + , os.index_lock_promotion_attempt_count + , os.index_lock_promotion_count + , os.page_latch_wait_count + , os.page_latch_wait_in_ms + , os.page_io_latch_wait_count + , os.page_io_latch_wait_in_ms + from ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats('+ CAST(@DatabaseID AS NVARCHAR(10)) +', NULL, NULL,NULL) AS os + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''finished getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; + '; + END; + ELSE + BEGIN + RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; + --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. + --If you have a lot of partitions and this suddenly starts running for a long time, change it back. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N', + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms)'; + + /* Get columnstore dictionary size - more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2585 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'column_store_dictionaries') + SET @dsql = @dsql + N' COALESCE((SELECT SUM (on_disk_size / 1024.0 / 1024) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries dict WHERE dict.partition_id = ps.partition_id),0) AS reserved_dictionary_MB '; + ELSE + SET @dsql = @dsql + N' 0 AS reserved_dictionary_MB '; - CREATE TABLE #ForeignKeys ( - [database_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - foreign_key_name NVARCHAR(256), - parent_object_id INT, - parent_object_name NVARCHAR(256), - referenced_object_id INT, - referenced_object_name NVARCHAR(256), - is_disabled BIT, - is_not_trusted BIT, - is_not_for_replication BIT, - parent_fk_columns NVARCHAR(MAX), - referenced_fk_columns NVARCHAR(MAX), - update_referential_action_desc NVARCHAR(16), - delete_referential_action_desc NVARCHAR(60) - ) - - CREATE TABLE #IndexCreateTsql ( - index_sanity_id INT NOT NULL, - create_tsql NVARCHAR(MAX) NOT NULL - ) - CREATE TABLE #DatabaseList ( - DatabaseName NVARCHAR(256), - secondary_role_allow_connections_desc NVARCHAR(50) + SET @dsql = @dsql + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' + + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ); + '; + END; - ) + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); - CREATE TABLE #PartitionCompressionInfo ( - [index_sanity_id] INT NULL, - [partition_compression_detail] VARCHAR(8000) NULL - ) + RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + EXEC sp_executesql @dsql; + INSERT #IndexPartitionSanity ( [database_id], + [object_id], + [schema_name], + index_id, + partition_number, + row_count, + reserved_MB, + reserved_LOB_MB, + reserved_row_overflow_MB, + lock_escalation_desc, + data_compression_desc, + leaf_insert_count, + leaf_delete_count, + leaf_update_count, + range_scan_count, + singleton_lookup_count, + forwarded_fetch_count, + lob_fetch_in_pages, + lob_fetch_in_bytes, + row_overflow_fetch_in_pages, + row_overflow_fetch_in_bytes, + row_lock_count, + row_lock_wait_count, + row_lock_wait_in_ms, + page_lock_count, + page_lock_wait_count, + page_lock_wait_in_ms, + index_lock_promotion_attempt_count, + index_lock_promotion_count, + page_latch_wait_count, + page_latch_wait_in_ms, + page_io_latch_wait_count, + page_io_latch_wait_in_ms, + reserved_dictionary_MB) + select h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc, + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms) + ,COALESCE((SELECT SUM (dict.on_disk_size / 1024.0 / 1024) FROM sys.column_store_dictionaries dict WHERE dict.partition_id = h.partition_id),0) AS reserved_dictionary_MB + from #dm_db_partition_stats_etc h + left JOIN #dm_db_index_operational_stats as os ON + h.object_id=os.object_id and h.index_id=os.index_id and h.partition_number=os.partition_number + group by h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.partition_id, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc + + END; --End Check For @SkipPartitions = 0 - CREATE TABLE #Statistics ( - database_id INT NOT NULL, - database_name NVARCHAR(256) NOT NULL, - table_name NVARCHAR(128) NULL, - schema_name NVARCHAR(128) NULL, - index_name NVARCHAR(128) NULL, - column_names NVARCHAR(4000) NULL, - statistics_name NVARCHAR(128) NULL, - last_statistics_update DATETIME NULL, - days_since_last_stats_update INT NULL, - rows BIGINT NULL, - rows_sampled BIGINT NULL, - percent_sampled DECIMAL(18, 1) NULL, - histogram_steps INT NULL, - modification_counter BIGINT NULL, - percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT 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 - ); - CREATE TABLE #ComputedColumns - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NULL, - database_id INT NOT NULL, - table_name NVARCHAR(128) NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - column_name NVARCHAR(128) NULL, - is_nullable BIT NULL, - definition NVARCHAR(MAX) NULL, - uses_database_collation BIT NOT NULL, - is_persisted BIT NOT NULL, - is_computed BIT NOT NULL, - is_function INT NOT NULL, - column_definition NVARCHAR(MAX) NULL - ); - - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); + IF @Mode NOT IN(1, 2) + BEGIN + RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; + SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' - CREATE TABLE #TemporalTables - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NOT NULL, - database_id INT NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - table_name NVARCHAR(128) NOT NULL, - history_table_name NVARCHAR(128) 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 - ); -/* Sanitize our inputs */ + SET @dsql = @dsql + ' +WITH + ColumnNamesWithDataTypes AS +( + SELECT + id.index_handle, + id.object_id, + cn.IndexColumnType, + STUFF + ( + ( + SELECT + '', '' + + cn_inner.ColumnName + + '' '' + + N'' {'' + + CASE + WHEN ty.name IN (''varchar'', ''char'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''nvarchar'', ''nchar'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length / 2 AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''decimal'', ''numeric'') + THEN ty.name + + ''('' + + CAST(co.precision AS VARCHAR(25)) + + '', '' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + WHEN ty.name IN (''datetime2'') + THEN ty.name + + ''('' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + ELSE ty.name END + ''}'' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id_inner + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(N''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + ) AS cn_inner + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS co + ON co.object_id = id_inner.object_id + AND ''['' + co.name + '']'' = cn_inner.ColumnName + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS ty + 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 cn_inner.IndexColumnType = cn.IndexColumnType + FOR XML PATH('''') + ), + 1, + 1, + '''' + ) AS ReplaceColumnNames + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + )AS cn + WHERE id.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + GROUP BY + id.index_handle, + id.object_id, + cn.IndexColumnType +) SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName) - - -IF @GetAllDatabases = 1 - BEGIN - INSERT INTO #DatabaseList (DatabaseName) - SELECT DB_NAME(database_id) - FROM sys.databases - WHERE user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE' - AND database_id > 4 - AND DB_NAME(database_id) NOT LIKE 'ReportServer%' - AND is_distributor = 0; + * +INTO #ColumnNamesWithDataTypes +FROM ColumnNamesWithDataTypes +OPTION(RECOMPILE); - /* Skip non-readable databases in an AG - see Github issue #1160 */ - IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') - BEGIN - SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( - SELECT d.name - FROM sys.dm_hadr_availability_replica_states rs - INNER JOIN sys.databases d ON rs.replica_id = d.replica_id - INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id - WHERE rs.role_desc = ''SECONDARY'' - AND r.secondary_role_allow_connections_desc = ''NO'');' - EXEC sp_executesql @dsql; +SELECT + id.database_id, + id.object_id, + @i_DatabaseName, + sc.[name], + so.[name], + id.statement, + gs.avg_total_user_cost, + gs.avg_user_impact, + gs.user_seeks, + gs.user_scans, + gs.unique_compiles, + id.equality_columns, + id.inequality_columns, + id.included_columns, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Equality'' + ) AS equality_columns_with_data_type, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Inequality'' + ) AS inequality_columns_with_data_type, + ( + SELECT ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Included'' + ) AS included_columns_with_data_type,'; + + /* Get the sample query plan if it's available, and if there are less than 1,000 rows in the DMV: */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.all_objects AS o + WHERE o.name = 'dm_db_missing_index_group_stats_query' + ) + SELECT + @dsql += N' + NULL AS sample_query_plan' + ELSE + BEGIN + /* The DMV is only supposed to have 600 rows in it. If it's got more, + they could see performance slowdowns - see Github #3085. */ + DECLARE @MissingIndexPlans BIGINT; + SET @StringToExecute = N'SELECT @MissingIndexPlans = COUNT(*) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query;' + EXEC sp_executesql @StringToExecute, N'@MissingIndexPlans BIGINT OUT', @MissingIndexPlans OUT; - IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') + IF @MissingIndexPlans > 1000 BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'Skipped non-readable AG secondary databases.', - N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', - N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', - 'http://FirstResponderKit.org', '', '', '', '' - ); + SELECT @dsql += N' + NULL AS sample_query_plan /* Over 1000 plans found, skipping */'; + RAISERROR (N'Over 1000 plans found in sys.dm_db_missing_index_group_stats_query - your SQL Server is hitting a bug: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3085',0,1) WITH NOWAIT; END - END - - END -ELSE - BEGIN - INSERT INTO #DatabaseList - ( DatabaseName ) - SELECT CASE WHEN @DatabaseName IS NULL OR @DatabaseName = N'' THEN DB_NAME() - ELSE @DatabaseName END - END + ELSE + SELECT + @dsql += N' + sample_query_plan = + ( + SELECT TOP (1) + p.query_plan + FROM sys.dm_db_missing_index_group_stats gs + CROSS APPLY + ( + SELECT TOP (1) + s.plan_handle + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query q + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_exec_query_stats s + ON q.query_plan_hash = s.query_plan_hash + WHERE gs.group_handle = q.group_handle + ORDER BY + (q.user_seeks + q.user_scans) DESC, + s.total_logical_reads DESC + ) q2 + CROSS APPLY sys.dm_exec_query_plan(q2.plan_handle) p + WHERE ig.index_group_handle = gs.group_handle + )' + END + + -SET @NumDatabases = @@ROWCOUNT; + SET @dsql = @dsql + N' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_groups ig +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details id + ON ig.index_handle = id.index_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats gs + ON ig.index_group_handle = gs.group_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so + ON id.object_id=so.object_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc + ON so.schema_id=sc.schema_id +WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + +CASE + WHEN @ObjectID IS NULL + THEN N'' + ELSE N' +AND id.object_id = ' + CAST(@ObjectID AS NVARCHAR(30)) +END + +N' +OPTION (RECOMPILE);'; -/* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, + avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, + inequality_columns, included_columns, equality_columns_with_data_type, inequality_columns_with_data_type, + included_columns_with_data_type, sample_query_plan) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; -BEGIN TRY - IF @NumDatabases >= 50 AND @BringThePain != 1 - BEGIN + SET @dsql = N' + SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') 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 + 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 + OPTION (RECOMPILE);'; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', - N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary insanity for the server and/or user.', - N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', - 'http://FirstResponderKit.org', '', '', '', '' - ); - - - SELECT bir.blitz_result_id, - bir.check_id, - bir.index_sanity_id, - bir.Priority, - bir.findings_group, - bir.finding, - bir.database_name, - bir.URL, - bir.details, - bir.index_definition, - bir.secret_columns, - bir.index_usage_summary, - bir.index_size_summary, - bir.create_tsql, - bir.more_info - FROM #BlitzIndexResults AS bir + RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, + is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, + [update_referential_action_desc], [delete_referential_action_desc] ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; - RETURN; + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql = N' + SELECT + DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') 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')), + fk.parent_object_id, + foreign_key_referenced_table = + OBJECT_NAME(fk.referenced_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + fk.referenced_object_id + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = fk.schema_id + WHERE fk.is_disabled = 0 + AND EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + WHERE fkc.constraint_object_id = fk.object_id + AND NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic + WHERE ic.object_id = fkc.parent_object_id + AND ic.column_id = fkc.parent_column_id + AND ic.index_column_id = fkc.constraint_column_id + ) + ) + OPTION (RECOMPILE);' + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); - END -END TRY -BEGIN CATCH - RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; + RAISERROR (N'Inserting data into #UnindexedForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + INSERT + #UnindexedForeignKeys + ( + database_id, + database_name, + schema_name, + foreign_key_name, + parent_object_name, + parent_object_id, + referenced_object_name, + referenced_object_id + ) + EXEC sys.sp_executesql + @dsql, + N'@i_DatabaseName sysname', + @DatabaseName; + END; - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); - - WHILE @@trancount > 0 - ROLLBACK; - RETURN; - END CATCH; + IF @Mode NOT IN(1, 2) + BEGIN + IF @SkipStatistics = 0 /* AND DB_NAME() = @DatabaseName /* Can only get stats in the current database - see https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1947 */ */ + BEGIN + IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) + OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) + OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) + 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, 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, 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, + ca.column_names AS column_names, + s.name AS statistics_name, + CONVERT(DATETIME, ddsp.last_updated) AS last_statistics_update, + DATEDIFF(DAY, ddsp.last_updated, GETDATE()) AS days_since_last_stats_update, + ddsp.rows, + ddsp.rows_sampled, + CAST(ddsp.rows_sampled / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) AS percent_sampled, + ddsp.steps AS histogram_steps, + ddsp.modification_counter, + CASE WHEN ddsp.modification_counter > 0 + THEN CAST(ddsp.modification_counter / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) + ELSE ddsp.modification_counter + END AS percent_modifications, + CASE WHEN ddsp.rows < 500 THEN 500 + 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, + ' + + 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 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch + ON sch.schema_id = obj.schema_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = s.object_id + AND i.index_id = s.stats_id + OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON sc.column_id = c.column_id AND sc.object_id = c.object_id + WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id + ORDER BY sc.stats_column_id + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') + ) ca (column_names) + WHERE obj.is_ms_shipped = 0 + OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); -/* Permission granted or unnecessary? Ok, let's go! */ + RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + ELSE + 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, 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, 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, + ca.column_names AS column_names, + s.name AS statistics_name, + CONVERT(DATETIME, STATS_DATE(s.object_id, s.stats_id)) AS last_statistics_update, + DATEDIFF(DAY, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_since_last_stats_update, + si.rowcnt, + si.rowmodctr, + CASE WHEN si.rowmodctr > 0 THEN CAST(si.rowmodctr / ( 1. * NULLIF(si.rowcnt, 0) ) * 100 AS DECIMAL(18, 1)) + ELSE si.rowmodctr + END AS percent_modifications, + CASE WHEN si.rowcnt < 500 THEN 500 + 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, + CONVERT(DATETIME, obj.modify_date) AS table_modify_date, + s.no_recompute, + ' + + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' + THEN N's.has_filter, + s.filter_definition,' + 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, + ' + + 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 + ON si.name = s.name AND s.object_id = si.id + INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj + ON s.object_id = obj.object_id + INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch + ON sch.schema_id = obj.schema_id + LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = s.object_id + AND i.index_id = s.stats_id + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON sc.column_id = c.column_id AND sc.object_id = c.object_id + WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id + ORDER BY sc.stats_column_id + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') + ) ca (column_names) + WHERE obj.is_ms_shipped = 0 + AND si.rowcnt > 0 + OPTION (RECOMPILE);'; -DECLARE c1 CURSOR -LOCAL FAST_FORWARD -FOR -SELECT DatabaseName FROM #DatabaseList WHERE COALESCE(secondary_role_allow_connections_desc, 'OK') <> 'NO' ORDER BY DatabaseName + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); -OPEN c1 -FETCH NEXT FROM c1 INTO @DatabaseName - WHILE @@FETCH_STATUS = 0 -BEGIN - - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@DatabaseName, 0, 1) WITH NOWAIT; + RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + END; + END; -SELECT @DatabaseID = [database_id] -FROM sys.databases - WHERE [name] = @DatabaseName - AND user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE'; + IF @Mode NOT IN(1, 2) + BEGIN + IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) + BEGIN + RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + t.name AS table_name, + s.name AS schema_name, + c.name AS column_name, + cc.is_nullable, + cc.definition, + cc.uses_database_collation, + cc.is_persisted, + cc.is_computed, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, + ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + + '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + + CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.computed_columns AS cc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON cc.object_id = c.object_id + AND cc.column_id = c.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = cc.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = t.schema_id + OPTION (RECOMPILE);'; -/* Last startup */ -SELECT @DaysUptime = CAST(DATEDIFF(hh,create_date,GETDATE())/24. AS NUMERIC (23,2)) -FROM sys.databases -WHERE database_id = 2; + IF @dsql IS NULL RAISERROR('@dsql is null',16,1); -IF @DaysUptime = 0 SET @DaysUptime = .01; + INSERT #ComputedColumns + ( database_id, [database_name], table_name, schema_name, column_name, is_nullable, definition, + uses_database_collation, is_persisted, is_computed, is_function, column_definition ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + END; ----------------------------------------- ---STEP 1: OBSERVE THE PATIENT ---This step puts index information into temp tables. ----------------------------------------- -BEGIN TRY - BEGIN + IF @Mode NOT IN(1, 2) + BEGIN + RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; + INSERT #TraceStatus + EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS'); - --Validate SQL Server Verson + IF (PARSENAME(@SQLServerProductVersion, 4) >= 13) + 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], + s.name AS schema_name, + t.name AS table_name, + oa.hsn as history_schema_name, + oa.htn AS history_table_name, + c1.name AS start_column_name, + c2.name AS end_column_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 + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c1 + ON t.object_id = c1.object_id + AND p.start_column_id = c1.column_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c2 + ON t.object_id = c2.object_id + AND p.end_column_id = c2.column_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + CROSS APPLY ( SELECT s2.name as hsn, t2.name htn + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t2 + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s2 + ON t2.schema_id = s2.schema_id + WHERE t2.object_id = t.history_table_id + AND t2.temporal_type = 1 /*History table*/ ) AS oa + WHERE t.temporal_type IN ( 2, 4 ) /*BOL currently points to these types, but has no definition for 4*/ + OPTION (RECOMPILE); + '; + + IF @dsql IS NULL + 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_object_id ) + + EXEC sp_executesql @dsql; + END; - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 - )) <= 9 - BEGIN - SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2008 and higher. The version of this instance is: ' + @SQLServerProductVersion; - RAISERROR(@msg,16,1); - END + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + t.name AS table_name, + s.name AS schema_name, + cc.name AS constraint_name, + cc.is_disabled, + cc.definition, + cc.uses_database_collation, + cc.is_not_trusted, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, + ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + + '' ADD CONSTRAINT '' + QUOTENAME(cc.name) + '' CHECK '' + cc.definition + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.check_constraints AS cc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = cc.parent_object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = t.schema_id + OPTION (RECOMPILE);'; + + INSERT #CheckConstraints + ( database_id, [database_name], table_name, schema_name, constraint_name, is_disabled, definition, + uses_database_collation, is_not_trusted, is_function, column_definition ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + s.name AS missing_schema_name, + t.name AS missing_table_name, + i.name AS missing_index_name, + c.name AS missing_column_name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.sql_expression_dependencies AS sed + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = sed.referenced_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = sed.referenced_id + AND i.index_id = sed.referencing_minor_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON c.object_id = sed.referenced_id + AND c.column_id = sed.referenced_minor_id + WHERE sed.referencing_class = 7 + AND sed.referenced_class = 1 + AND i.has_filter = 1 + AND NOT EXISTS ( SELECT 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS ic + WHERE ic.index_id = sed.referencing_minor_id + AND ic.column_id = sed.referenced_minor_id + AND ic.object_id = sed.referenced_id ) + OPTION(RECOMPILE);' - --Short circuit here if database name does not exist. - IF @DatabaseName IS NULL OR @DatabaseID IS NULL - BEGIN - SET @msg='Database does not exist or is not online/multi-user: cannot proceed.' - RAISERROR(@msg,16,1); - END + BEGIN TRY + INSERT #FilteredIndexes ( database_id, database_name, schema_name, table_name, index_name, column_name ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END TRY + BEGIN CATCH + RAISERROR (N'Skipping #FilteredIndexes population due to error, typically low permissions.', 0,1) WITH NOWAIT; + END CATCH + END; - --Validate parameters. - IF (@Mode NOT IN (0,1,2,3,4)) + 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 @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail, 4=diagnose detail'; - RAISERROR(@msg,16,1); - END + 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);' - IF (@Mode <> 0 AND @TableName IS NOT NULL) - BEGIN - SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; - RAISERROR(@msg,16,1); - END + 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; - IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) - BEGIN - SET @msg=N'@Filter only appies when @Mode=0 and @TableName is not specified. Please try again.'; - RAISERROR(@msg,16,1); - END - IF (@SchemaName IS NOT NULL AND @TableName IS NULL) + END; + +END; +END TRY +BEGIN CATCH + RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL BEGIN - SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.' - RAISERROR(@msg,16,1); - END + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END; + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; + + + WHILE @@trancount > 0 + ROLLBACK; - IF (@TableName IS NOT NULL AND @SchemaName IS NULL) - BEGIN - SET @SchemaName=N'dbo' - SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.' - RAISERROR(@msg,1,1) WITH NOWAIT; - END + RETURN; +END CATCH; + FETCH NEXT FROM c1 INTO @DatabaseName; +END; +DEALLOCATE c1; - --If a table is specified, grab the object id. - --Short circuit if it doesn't exist. - IF @TableName IS NOT NULL - BEGIN - SET @dsql = N' - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @ObjectID= OBJECT_ID - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on - so.schema_id=sc.schema_id - where so.type in (''U'', ''V'') - and so.name=' + QUOTENAME(@TableName,'''')+ N' - and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' - /*Has a row in sys.indexes. This lets us get indexed views.*/ - and exists ( - SELECT si.name - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si - WHERE so.object_id=si.object_id) - OPTION (RECOMPILE);'; - SET @params='@ObjectID INT OUTPUT' - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; - - IF @ObjectID IS NULL - BEGIN - SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + - N'Please check your parameters.' - RAISERROR(@msg,1,1); - RETURN; - END - END - --set @collation - SELECT @collation=collation_name - FROM sys.databases - WHERE database_id=@DatabaseID; - --insert columns for clustered indexes and heaps - --collect info on identity columns for this one - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', - CAST(ic.seed_value AS BIGINT), - CAST(ic.increment_value AS INT), - CAST(ic.last_value AS BIGINT), - ic.is_not_for_replication - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON - si.object_id=c.object_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON - c.object_id=ic.object_id and - c.column_id=ic.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; +---------------------------------------- +--STEP 2: PREP THE TEMP TABLES +--EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. +---------------------------------------- - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); +RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names = D1.key_column_names +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') ,TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D1 ( key_column_names ); - RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) - EXEC sp_executesql @dsql; +RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET partition_key_column_name = D1.partition_key_column_name +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 + ( partition_key_column_name ); - --insert columns for nonclustered indexes - --this uses a full join to sys.index_columns - --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL AS is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL AS is_filestream' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON - si.object_id=c.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id not in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; +RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key + WHEN 1 THEN N' DESC' + ELSE N'' + END + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order ); - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); +RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key + WHEN 1 THEN N' DESC' + ELSE N'' + END AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order_no_types ); + +RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET include_column_names = D3.include_column_names +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D3 ( include_column_names ); + +RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET include_column_names_no_types = D3.include_column_names_no_types +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D3 ( include_column_names_no_types ); - RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream ) - EXEC sp_executesql @dsql; - - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - so.object_id, - si.index_id, - si.type, - ' + QUOTENAME(@DatabaseName, '''') + ' AS database_name, - COALESCE(sc.NAME, ''Unknown'') AS [schema_name], - COALESCE(so.name, ''Unknown'') AS [object_name], - COALESCE(si.name, ''Unknown'') AS [index_name], - CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, - si.is_unique, - si.is_primary_key, - CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, - 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, - si.is_disabled, - si.is_hypothetical, - si.is_padded, - si.fill_factor,' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN ' - CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition - ELSE '''' - END AS filter_definition' ELSE ''''' AS filter_definition' END + ' - , ISNULL(us.user_seeks, 0), ISNULL(us.user_scans, 0), - ISNULL(us.user_lookups, 0), ISNULL(us.user_updates, 0), us.last_user_seek, us.last_user_scan, - us.last_user_lookup, us.last_user_update, - so.create_date, so.modify_date - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si WITH (NOLOCK) - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas sc ON so.schema_id = sc.schema_id - 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)) + ' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + - CASE WHEN @TableName IS NOT NULL THEN ' and so.name=' + QUOTENAME(@TableName,'''') + ' ' ELSE '' END + - 'OPTION ( RECOMPILE ); - '; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); +RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET count_included_columns = D4.count_included_columns, + count_key_columns = D4.count_key_columns +FROM #IndexSanity si + CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 + ELSE 0 + END) AS count_included_columns, + SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 + ELSE 0 + END) AS count_key_columns + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + ) AS D4 ( count_included_columns, count_key_columns ); - RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; - 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_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, - is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, user_seeks, user_scans, - user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, - create_date, modify_date ) - EXEC sp_executesql @dsql; +RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; +UPDATE #IndexPartitionSanity +SET index_sanity_id = i.index_sanity_id +FROM #IndexPartitionSanity ps + JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] + AND ps.index_id = i.index_id + AND i.database_id = ps.database_id + AND i.schema_name = ps.schema_name; - RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; - IF @BringThePain = 0 AND @SkipPartitions = 0 - BEGIN - /* Count the total number of partitions */ - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; - EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; - IF @Rowcount > 100 - BEGIN - RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',16,1) WITH NOWAIT; - SET @SkipPartitions = 1; - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'Some Checks Were Skipped', - '@SkipPartitions Forced to 1', - 'http://FirstResponderKit.org', CAST(@Rowcount AS VARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' - ); - END - END +RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; +INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], [lock_escalation_desc], partition_count, total_rows, total_reserved_MB, + total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_reserved_dictionary_MB, total_range_scan_count, + total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, + total_forwarded_fetch_count,total_row_lock_count, + total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, + total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, + avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, + total_index_lock_promotion_count, data_compression_desc, + page_latch_wait_count, page_latch_wait_in_ms, page_io_latch_wait_count, page_io_latch_wait_in_ms) + SELECT index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc, + COUNT(*), SUM(row_count), SUM(reserved_MB), + SUM(reserved_LOB_MB) - SUM(reserved_dictionary_MB), /* Subtract columnstore dictionaries from LOB data */ + SUM(reserved_row_overflow_MB), + SUM(reserved_dictionary_MB), + SUM(range_scan_count), + SUM(singleton_lookup_count), + SUM(leaf_delete_count), + SUM(leaf_update_count), + SUM(forwarded_fetch_count), + SUM(row_lock_count), + SUM(row_lock_wait_count), + SUM(row_lock_wait_in_ms), + CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN + SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) + ELSE 0 END AS avg_row_lock_wait_in_ms, + SUM(page_lock_count), + SUM(page_lock_wait_count), + SUM(page_lock_wait_in_ms), + CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN + SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) + ELSE 0 END AS avg_page_lock_wait_in_ms, + SUM(index_lock_promotion_attempt_count), + SUM(index_lock_promotion_count), + LEFT(MAX(data_compression_info.data_compression_rollup),4000), + SUM(page_latch_wait_count), + SUM(page_latch_wait_in_ms), + SUM(page_io_latch_wait_count), + SUM(page_io_latch_wait_in_ms) + FROM #IndexPartitionSanity ipp + /* individual partitions can have distinct compression settings, just roll them into a list here*/ + OUTER APPLY (SELECT STUFF(( + SELECT N', ' + data_compression_desc + FROM #IndexPartitionSanity ipp2 + WHERE ipp.[object_id]=ipp2.[object_id] + AND ipp.[index_id]=ipp2.[index_id] + AND ipp.database_id = ipp2.database_id + AND ipp.schema_name = ipp2.schema_name + ORDER BY ipp2.partition_number + FOR XML PATH(''),TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + data_compression_info(data_compression_rollup) + GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc + ORDER BY index_sanity_id +OPTION ( RECOMPILE ); +RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; +UPDATE #MissingIndexes +SET is_low = CASE WHEN (user_seeks + user_scans) < 5000 + OR unique_compiles = 1 + THEN 1 + ELSE 0 + END; +RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; +UPDATE #IndexSanity + SET is_referenced_by_foreign_key=1 +FROM #IndexSanity s +JOIN #ForeignKeys fk ON + s.object_id=fk.referenced_object_id + AND s.database_id=fk.database_id + AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns; - IF (@SkipPartitions = 0) - BEGIN - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 )) <= 2147483647 --Make change here - BEGIN - - RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; +RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; +UPDATE nc +SET secret_columns= + N'[' + + CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS NVARCHAR(10)) END + + CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + + CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + + CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + + /* Uniquifiers only needed on non-unique clustereds-- not heaps */ + CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END + END + , count_secret_columns= + CASE tb.index_id WHEN 0 THEN 1 ELSE + tb.count_key_columns + + CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END + END +FROM #IndexSanity AS nc +JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id + AND nc.database_id = tb.database_id + AND nc.schema_name = tb.schema_name + AND tb.index_id IN (0,1) +WHERE nc.index_id > 1; - --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 - --This change was made because on a table with lots of paritions, the OUTER APPLY was crazy slow. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN 'par.data_compression_desc ' ELSE 'null as data_compression_desc' END + ' - FROM ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + ', NULL, NULL,NULL) AS os ON - ps.object_id=os.object_id and ps.index_id=os.index_id and ps.partition_number=os.partition_number - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + ' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END - ELSE - BEGIN - RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. - --If you have a lot of paritions and this suddenly starts running for a long time, change it back. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END; +RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; +UPDATE tb +SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END + , count_secret_columns = 1 +FROM #IndexSanity AS tb +WHERE tb.index_id = 0 /*Heaps-- these have the RID */ + OR (tb.index_id=1 AND tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; - INSERT #IndexPartitionSanity ( [database_id], - [object_id], - [schema_name], - index_id, - partition_number, - row_count, - reserved_MB, - reserved_LOB_MB, - reserved_row_overflow_MB, - leaf_insert_count, - leaf_delete_count, - leaf_update_count, - range_scan_count, - singleton_lookup_count, - forwarded_fetch_count, - lob_fetch_in_pages, - lob_fetch_in_bytes, - row_overflow_fetch_in_pages, - row_overflow_fetch_in_bytes, - row_lock_count, - row_lock_wait_count, - row_lock_wait_in_ms, - page_lock_count, - page_lock_wait_count, - page_lock_wait_in_ms, - index_lock_promotion_attempt_count, - index_lock_promotion_count, - data_compression_desc ) - EXEC sp_executesql @dsql; - - END; --End Check For @SkipPartitions = 0 +RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; +INSERT #IndexCreateTsql (index_sanity_id, create_tsql) +SELECT + index_sanity_id, + ISNULL ( + CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' + ELSE + CASE WHEN is_XML = 1 OR is_spatial = 1 OR is_in_memory_oltp = 1 THEN N'' /* Not even trying for these just yet...*/ + ELSE + CASE WHEN is_primary_key=1 THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] PRIMARY KEY ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_unique_constraint = 1 AND is_primary_key = 0 + THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] UNIQUE ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_CX_columnstore= 1 THEN + N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ELSE /*Else not a PK or cx columnstore */ + N'CREATE ' + + CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + + CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + + CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' + ELSE N'' END + + N'INDEX [' + + index_name + N'] ON ' + + QUOTENAME([database_name]) + N'.' + + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + + CASE WHEN is_NC_columnstore=1 THEN + N' (' + ISNULL(include_column_names_no_types,'') + N' )' + ELSE /*Else not columnstore */ + N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' + + CASE WHEN include_column_names_no_types IS NOT NULL THEN + N' INCLUDE (' + include_column_names_no_types + N')' + ELSE N'' + END + END /*End non-columnstore case */ + + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END + END /*End Non-PK index CASE */ + + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN + N' WITH (' + + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' + + N'ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' + + N')' + ELSE N'' END + + N';' + END /*End non-spatial and non-xml CASE */ + END, '[Unknown Error]') + AS create_tsql +FROM #IndexSanity; + +RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#maps') IS NOT NULL DROP TABLE #maps; +WITH maps + AS + ( + SELECT ips.index_sanity_id, + ips.partition_number, + ips.data_compression_desc, + ips.partition_number - ROW_NUMBER() OVER ( PARTITION BY ips.index_sanity_id, ips.data_compression_desc + ORDER BY ips.partition_number ) AS rn + FROM #IndexPartitionSanity AS ips + ) +SELECT * +INTO #maps +FROM maps; + +IF OBJECT_ID('tempdb..#grps') IS NOT NULL DROP TABLE #grps; +WITH grps + AS + ( + SELECT MIN(maps.partition_number) AS MinKey, + MAX(maps.partition_number) AS MaxKey, + maps.index_sanity_id, + maps.data_compression_desc + FROM #maps AS maps + GROUP BY maps.rn, maps.index_sanity_id, maps.data_compression_desc + ) +SELECT * +INTO #grps +FROM grps; + +INSERT #PartitionCompressionInfo ( index_sanity_id, partition_compression_detail ) +SELECT DISTINCT + grps.index_sanity_id, + SUBSTRING( + ( STUFF( + ( SELECT N', ' + N' Partition' + + CASE + WHEN grps2.MinKey < grps2.MaxKey + THEN + + N's ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' - ' + + CAST(grps2.MaxKey AS NVARCHAR(10)) + N' use ' + grps2.data_compression_desc + ELSE + N' ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' uses ' + grps2.data_compression_desc + END AS Partitions + FROM #grps AS grps2 + WHERE grps2.index_sanity_id = grps.index_sanity_id + ORDER BY grps2.MinKey, grps2.MaxKey + FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'), 1, 1, '')), 0, 8000) AS partition_compression_detail +FROM #grps AS grps; + +RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; +UPDATE sz +SET sz.data_compression_desc = pci.partition_compression_detail +FROM #IndexSanitySize sz +JOIN #PartitionCompressionInfo AS pci +ON pci.index_sanity_id = sz.index_sanity_id; +RAISERROR (N'Update #IndexSanity for filtered indexes with columns not in the index definition.',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET filter_columns_not_in_index = D1.filter_columns_not_in_index +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #FilteredIndexes AS c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.table_name = si.object_name + AND c.index_name = si.index_name + ORDER BY c.index_sanity_id + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 + ( filter_columns_not_in_index ); - RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT id.database_id, id.object_id, ' + QUOTENAME(@DatabaseName,'''') + N', sc.[name], so.[name], id.statement , gs.avg_total_user_cost, - gs.avg_user_impact, gs.user_seeks, gs.user_scans, gs.unique_compiles,id.equality_columns, - id.inequality_columns,id.included_columns - FROM sys.dm_db_missing_index_groups ig - JOIN sys.dm_db_missing_index_details id ON ig.index_handle = id.index_handle - JOIN sys.dm_db_missing_index_group_stats gs ON ig.index_group_handle = gs.group_handle - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so on - id.object_id=so.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc on - so.schema_id=sc.schema_id - WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + ' - ' + CASE WHEN @ObjectID IS NULL THEN N'' - ELSE N'and id.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - END + - N'OPTION (RECOMPILE);' +IF @Debug = 1 +BEGIN + SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults AS bir; + SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; + SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; + SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; + SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; + SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; + SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; + SELECT '#UnindexedForeignKeys' AS table_name, * FROM #UnindexedForeignKeys; + SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; + SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; + 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 '#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; +END - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, - avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, - inequality_columns, included_columns) - EXEC sp_executesql @dsql; - SET @dsql = N' - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' 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 - 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(''.'', ''varchar(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(''.'', ''varchar(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); +---------------------------------------- +--STEP 3: DIAGNOSE THE PATIENT +---------------------------------------- - RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; - INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, - is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, - [update_referential_action_desc], [delete_referential_action_desc] ) - EXEC sp_executesql @dsql; +BEGIN TRY +---------------------------------------- +--If @TableName is specified, just return information for that table. +--The @Mode parameter doesn't matter if you're looking at a specific table. +---------------------------------------- +IF @TableName IS NOT NULL +BEGIN + RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; - IF @SkipStatistics = 0 - BEGIN - IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) - OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) - OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) - BEGIN - RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, ddsp.last_updated) AS last_statistics_update, - DATEDIFF(DAY, ddsp.last_updated, GETDATE()) AS days_since_last_stats_update, - ddsp.rows, - ddsp.rows_sampled, - CAST(ddsp.rows_sampled / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) AS percent_sampled, - ddsp.steps AS histogram_steps, - ddsp.modification_counter, - CASE WHEN ddsp.modification_counter > 0 - THEN CAST(ddsp.modification_counter / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE ddsp.modification_counter - END AS percent_modifications, - CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) - 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 - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); + --We do a left join here in case this is a disabled NC. + --In that case, it won't have any size info/pages allocated. + + IF (@ShowColumnstoreOnly = 0) + BEGIN + WITH table_mode_cte AS ( + SELECT + s.db_schema_object_indexid, + s.key_column_names, + s.index_definition, + ISNULL(s.secret_columns,N'') AS secret_columns, + s.fill_factor, + s.index_usage_summary, + sz.index_op_stats, + ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, + partition_compression_detail , + ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, + s.is_referenced_by_foreign_key, + (SELECT COUNT(*) + FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id + AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, + s.last_user_seek, + s.last_user_scan, + s.last_user_lookup, + s.last_user_update, + s.create_date, + s.modify_date, + sz.page_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_latch_wait_in_ms / 1000), 0), 108) AS page_latch_wait_time, + sz.page_io_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_io_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_io_latch_wait_in_ms / 1000), 0), 108) AS page_io_latch_wait_time, + ct.create_tsql, + CASE + WHEN s.is_primary_key = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND is_unique_constraint = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND s.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(s.index_name) + N' ON ' + QUOTENAME(s.[database_name]) + N'.' + + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + N';' + ELSE N'' + END AS drop_tsql, + 1 AS display_order + FROM #IndexSanity s + LEFT JOIN #IndexSanitySize sz ON + s.index_sanity_id=sz.index_sanity_id + LEFT JOIN #IndexCreateTsql ct ON + s.index_sanity_id=ct.index_sanity_id + LEFT JOIN #PartitionCompressionInfo pci ON + pci.index_sanity_id = s.index_sanity_id + WHERE s.[object_id]=@ObjectID + UNION ALL + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + + N' (' + @ScriptVersionName + ')' , + N'SQL Server First Responder Kit' , + N'http://FirstResponderKit.org' , + N'From Your Community Volunteers', + NULL,@DaysUptimeInsertValue,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + 0 AS display_order + ) + SELECT + db_schema_object_indexid AS [Details: db_schema.table.index(indexid)], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + secret_columns AS [Secret Columns], + fill_factor AS [Fillfactor], + index_usage_summary AS [Usage Stats], + index_op_stats AS [Op Stats], + index_size_summary AS [Size], + partition_compression_detail AS [Compression Type], + index_lock_wait_summary AS [Lock Waits], + is_referenced_by_foreign_key AS [Referenced by FK?], + FKs_covered_by_index AS [FK Covered by Index?], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Write], + create_date AS [Created], + modify_date AS [Last Modified], + page_latch_wait_count AS [Page Latch Wait Count], + page_latch_wait_time as [Page Latch Wait Time (D:H:M:S)], + page_io_latch_wait_count AS [Page IO Latch Wait Count], + page_io_latch_wait_time as [Page IO Latch Wait Time (D:H:M:S)], + create_tsql AS [Create TSQL], + drop_tsql AS [Drop TSQL] + FROM table_mode_cte + ORDER BY display_order ASC, key_column_names ASC + OPTION ( RECOMPILE ); + + IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL + BEGIN; + + WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + SELECT N'Missing index.' AS Finding , + N'https://www.brentozar.com/go/Indexaphobia' AS URL , + mi.[statement] + + ' Est. Benefit: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) + AS BIGINT) AS MONEY), 1), '.00', '') + END AS [Estimated Benefit], + missing_index_details AS [Missing Index Request] , + index_estimated_impact AS [Estimated Impact], + create_tsql AS [Create TSQL], + sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + WHERE mi.[object_id] = @ObjectID + AND (@ShowAllMissingIndexRequests=1 + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + OR (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000) + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); + END; + ELSE + SELECT 'No missing indexes.' AS finding; + + SELECT + column_name AS [Column Name], + (SELECT COUNT(*) + FROM #IndexColumns c2 + WHERE c2.column_name=c.column_name + AND c2.key_ordinal IS NOT NULL) + + CASE WHEN c.index_id = 1 AND c.key_ordinal IS NOT NULL THEN + -1+ (SELECT COUNT(DISTINCT index_id) + FROM #IndexColumns c3 + WHERE c3.index_id NOT IN (0,1)) + ELSE 0 END + AS [Found In], + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE '' + END + END + AS [Type], + CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], + max_length AS [Length (max bytes)], + [precision] AS [Prec], + [scale] AS [Scale], + CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], + CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], + CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], + CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], + CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], + collation_name AS [Collation] + FROM #IndexColumns AS c + WHERE index_id IN (0,1); + + IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL + BEGIN + SELECT [database_name] + N':' + parent_object_name + N': ' + foreign_key_name AS [Foreign Key], + parent_fk_columns AS [Foreign Key Columns], + referenced_object_name AS [Referenced Table], + referenced_fk_columns AS [Referenced Table Columns], + is_disabled AS [Is Disabled?], + is_not_trusted AS [Not Trusted?], + is_not_for_replication [Not for Replication?], + [update_referential_action_desc] AS [Cascading Updates?], + [delete_referential_action_desc] AS [Cascading Deletes?] + FROM #ForeignKeys + ORDER BY [Foreign Key] + OPTION ( RECOMPILE ); + END; + ELSE + SELECT 'No foreign keys.' AS finding; - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END - ELSE - BEGIN - RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, STATS_DATE(s.object_id, s.stats_id)) AS last_statistics_update, - DATEDIFF(DAY, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_since_last_stats_update, - si.rowcnt, - si.rowmodctr, - CASE WHEN si.rowmodctr > 0 THEN CAST(si.rowmodctr / ( 1. * NULLIF(si.rowcnt, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE si.rowmodctr - END AS percent_modifications, - CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) - 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, - ' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' - THEN N's.has_filter, - s.filter_definition' - ELSE N'NULL AS has_filter, - NULL AS filter_definition' END - + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si - ON si.name = s.name - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - AND si.rowcnt > 0 - OPTION (RECOMPILE);' + /* Show histograms for all stats on this table. More info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1900 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_histogram') + BEGIN + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT s.name AS [Stat Name], c.name AS [Leading Column Name], hist.step_number AS [Step Number], + hist.range_high_key AS [Range High Key], hist.range_rows AS [Range Rows], + hist.equal_rows AS [Equal Rows], hist.distinct_range_rows AS [Distinct Range Rows], hist.average_range_rows AS [Average Range Rows], + s.auto_created AS [Auto-Created], s.user_created AS [User-Created], + props.last_updated AS [Last Updated], props.modification_counter AS [Modification Counter], props.rows AS [Table Rows], + props.rows_sampled AS [Rows Sampled], s.stats_id AS [StatsID] + FROM sys.stats AS s + INNER JOIN sys.stats_columns sc ON s.object_id = sc.object_id AND s.stats_id = sc.stats_id AND sc.stats_column_id = 1 + INNER JOIN sys.columns c ON sc.object_id = c.object_id AND sc.column_id = c.column_id + CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS props + CROSS APPLY sys.dm_db_stats_histogram(s.[object_id], s.stats_id) AS hist + WHERE s.object_id = @ObjectID + ORDER BY s.auto_created, s.user_created, s.name, hist.step_number;'; + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + 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; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); + END /* END @ShowColumnstoreOnly = 0 */ - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END + /* 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')) + BEGIN + RAISERROR(N'Visualizing columnstore index contents.', 0,1) WITH NOWAIT; - END + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + IF EXISTS(SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = @ObjectID) + BEGIN + SELECT @ColumnList = N'''', @ColumnListWithApostrophes = N''''; + WITH DistinctColumns AS ( + SELECT DISTINCT QUOTENAME(c.name) AS column_name, QUOTENAME(c.name,'''''''') AS ColumnNameWithApostrophes, c.column_id + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON p.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id and ic.object_id = c.object_id AND ic.index_id = p.index_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types t ON c.system_type_id = t.system_type_id AND c.user_type_id = t.user_type_id + WHERE p.object_id = @ObjectID + AND EXISTS (SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg WHERE p.partition_id = seg.partition_id AND seg.column_id = ic.index_column_id) + AND p.data_compression IN (3,4) + ) + SELECT @ColumnList = @ColumnList + column_name + N'', '', + @ColumnListWithApostrophes = @ColumnListWithApostrophes + ColumnNameWithApostrophes + N'', '' + FROM DistinctColumns + ORDER BY column_id; + SELECT @PartitionCount = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions WHERE object_id = @ObjectID AND data_compression IN (3,4); + END'; - IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) + IF @Debug = 1 BEGIN - RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - t.name AS table_name, - s.name AS schema_name, - c.name AS column_name, - cc.is_nullable, - cc.definition, - cc.uses_database_collation, - cc.is_persisted, - cc.is_computed, - CASE WHEN cc.definition LIKE ''%.%'' THEN 1 ELSE 0 END AS is_function, - ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + - '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + - CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.computed_columns AS cc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON cc.object_id = c.object_id - AND cc.column_id = c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON t.object_id = cc.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON s.schema_id = t.schema_id - OPTION (RECOMPILE);' + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); + EXEC sp_executesql @dsql, N'@ObjectID INT, @ColumnList NVARCHAR(MAX) OUTPUT, @ColumnListWithApostrophes NVARCHAR(MAX) OUTPUT, @PartitionCount INT OUTPUT', @ObjectID, @ColumnList OUTPUT, @ColumnListWithApostrophes OUTPUT, @PartitionCount OUTPUT; - INSERT #ComputedColumns - ( [database_name], database_id, table_name, schema_name, column_name, is_nullable, definition, - uses_database_collation, is_persisted, is_computed, is_function, column_definition ) - EXEC sp_executesql @dsql; + IF @PartitionCount < 2 + SET @ShowPartitionRanges = 0; - END - - RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; - INSERT #TraceStatus - EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS') + IF @Debug = 1 + SELECT @ColumnList AS ColumnstoreColumnList, @ColumnListWithApostrophes AS ColumnstoreColumnListWithApostrophes, @PartitionCount AS PartitionCount, @ShowPartitionRanges AS ShowPartitionRanges; - IF (PARSENAME(@SQLServerProductVersion, 4) >= 13) - BEGIN - RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - s.name AS schema_name, - t.name AS table_name, - oa.hsn as history_schema_name, - oa.htn AS history_table_name, - c1.name AS start_column_name, - c2.name AS end_column_name, - p.name AS period_name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.periods AS p - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON p.object_id = t.object_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c1 - ON t.object_id = c1.object_id - AND p.start_column_id = c1.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c2 - ON t.object_id = c2.object_id - AND p.end_column_id = c2.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON t.schema_id = s.schema_id - CROSS APPLY ( SELECT s2.name as hsn, t2.name htn - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t2 - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s2 - ON t2.schema_id = s2.schema_id - WHERE t2.object_id = t.history_table_id - AND t2.temporal_type = 1 /*History table*/ ) AS oa - WHERE t.temporal_type IN ( 2, 4 ) /*BOL currently points to these types, but has no definition for 4*/ - OPTION (RECOMPILE); - ' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_table_name, - history_schema_name, start_column_name, end_column_name, period_name ) - - EXEC sp_executesql @dsql; + IF @ColumnList <> '' + BEGIN + /* Remove the trailing comma */ + SET @ColumnList = LEFT(@ColumnList, LEN(@ColumnList) - 1); + SET @ColumnListWithApostrophes = LEFT(@ColumnListWithApostrophes, LEN(@ColumnListWithApostrophes) - 1); + + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT partition_number, ' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' COALESCE(range_start_op + '' '' + range_start + '' '', '''') + COALESCE(range_end_op + '' '' + range_end, '''') AS partition_range, ' ELSE N' ' END + + N' row_group_id, total_rows, deleted_rows, ' + + @ColumnList + + CASE WHEN @ShowPartitionRanges = 1 THEN N' , + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT column_name, partition_number, row_group_id, total_rows, deleted_rows, details, + range_start_op, + CASE + WHEN format_type IS NULL THEN CAST(range_start_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_start_value, format_type) END range_start, + range_end_op, + CASE + WHEN format_type IS NULL THEN CAST(range_end_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_end_value, format_type) END range_end' ELSE N' ' END + N', + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT c.name AS column_name, p.partition_number, rg.row_group_id, rg.total_rows, rg.deleted_rows, + phys.state_desc, phys.trim_reason_desc, phys.transition_to_compressed_state_desc, phys.has_vertipaq_optimization, + details = CAST(seg.min_data_id AS VARCHAR(20)) + '' to '' + CAST(seg.max_data_id AS VARCHAR(20)) + '', '' + CAST(CAST(((COALESCE(d.on_disk_size,0) + COALESCE(seg.on_disk_size,0)) / 1024.0 / 1024) AS DECIMAL(18,0)) AS VARCHAR(20)) + '' MB''' + + CASE WHEN @ShowPartitionRanges = 1 THEN N', + CASE + WHEN pp.system_type_id IN (40, 41, 42, 43, 58, 61) THEN 126 + WHEN pp.system_type_id IN (59, 62) THEN 3 + WHEN pp.system_type_id IN (60, 122) THEN 2 + ELSE NULL END format_type, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''>'' ELSE ''>='' END range_start_op, + prvs.value range_start_value, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''<='' ELSE ''<'' END range_end_op, + prve.value range_end_value ' ELSE N' ' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups rg + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON rg.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p ON rg.object_id = p.object_id AND rg.partition_number = p.partition_number + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id AND ic.object_id = c.object_id AND ic.index_id = p.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_column_store_row_group_physical_stats phys ON rg.row_group_id = phys.row_group_id AND rg.object_id = phys.object_id AND rg.partition_number = phys.partition_number AND rg.index_id = phys.index_id ' + CASE WHEN @ShowPartitionRanges = 1 THEN N' + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes i ON i.object_id = rg.object_id AND i.index_id = rg.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_schemes ps ON ps.data_space_id = i.data_space_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_functions pf ON pf.function_id = ps.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_parameters pp ON pp.function_id = pf.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prvs ON prvs.function_id = pf.function_id AND prvs.boundary_id = p.partition_number - 1 + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prve ON prve.function_id = pf.function_id AND prve.boundary_id = p.partition_number ' ELSE N' ' END + + N' LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg ON p.partition_id = seg.partition_id AND ic.index_column_id = seg.column_id AND rg.row_group_id = seg.segment_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries d ON p.hobt_id = d.hobt_id AND c.column_id = d.column_id AND seg.secondary_dictionary_id = d.dictionary_id + WHERE rg.object_id = @ObjectID + AND rg.state IN (1, 2, 3) + AND c.name IN ( ' + @ColumnListWithApostrophes + N')' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' + ) AS y ' ELSE N' ' END + N' + ) AS x + PIVOT (MAX(details) FOR column_name IN ( ' + @ColumnList + N')) AS pivot1 + ORDER BY partition_number, row_group_id;'; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + ELSE + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + END + ELSE /* No columns were found for this object */ + BEGIN + SELECT N'No compressed columnstore rowgroups were found for this object.' AS Columnstore_Visualization + UNION ALL + SELECT N'SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = ' + CAST(@ObjectID AS NVARCHAR(100)); + END + RAISERROR(N'Done visualizing columnstore index contents.', 0,1) WITH NOWAIT; END - -END -END TRY -BEGIN CATCH - RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; - IF @dsql IS NOT NULL - BEGIN - SET @msg= 'Last @dsql: ' + @dsql; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END + IF @ShowColumnstoreOnly = 1 + RETURN; - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; - - - WHILE @@trancount > 0 - ROLLBACK; +END; /* IF @TableName IS NOT NULL */ - RETURN; -END CATCH; - FETCH NEXT FROM c1 INTO @DatabaseName -END -DEALLOCATE c1; ----------------------------------------- ---STEP 2: PREP THE TEMP TABLES ---EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. ----------------------------------------- -RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names = D1.key_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') ,TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D1 ( key_column_names ) +ELSE /* @TableName IS NULL, so we operate in normal mode 0/1/2/3/4 */ +BEGIN + +/* Validate and check table output params */ + + + /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ + DECLARE @ValidOutputServer BIT; + DECLARE @ValidOutputLocation BIT; + DECLARE @LinkedServerDBCheck NVARCHAR(2000); + DECLARE @ValidLinkedServerDB INT; + DECLARE @tmpdbchk TABLE (cnt INT); + + IF @OutputServerName IS NOT NULL + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; + ELSE + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + EXEC(@StringToExecute); + + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + SET @OutputDatabaseName = '[tempdb]'; + SET @OutputSchemaName = '[dbo]'; + SET @ValidOutputLocation = 1; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + IF (@ValidOutputLocation = 0 AND @OutputType = 'NONE') + BEGIN + RAISERROR('Invalid output location and no output asked',12,1); + RETURN; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @RunID UNIQUEIDENTIFIER; + SET @RunID = NEWID(); -RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET partition_key_column_name = D1.partition_key_column_name -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1,''))) D1 - ( partition_key_column_name ) + DECLARE @TableExists BIT; + DECLARE @SchemaExists BIT; -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order ) + DECLARE @TableExistsSql NVARCHAR(MAX); -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order_no_types ) + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + SET @StringToExecute = + N'SET @SchemaExists = 0; + SET @TableExists = 0; + IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + SET @SchemaExists = 1 + IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + BEGIN + SET @TableExists = 1 + IF NOT EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.COLUMNS WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' + AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'' AND QUOTENAME(COLUMN_NAME) = ''[total_forwarded_fetch_count]'') + EXEC @@@OutputServerName@@@.@@@OutputDatabaseName@@@.dbo.sp_executesql N''ALTER TABLE @@@OutputSchemaName@@@.@@@OutputTableName@@@ ADD [total_forwarded_fetch_count] BIGINT'' + END'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + + EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT; -RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names = D3.include_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names ); -RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names_no_types = D3.include_column_names_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names_no_types ); + SET @TableExistsSql = + N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + SET @TableExists = 0 + ELSE + SET @TableExists = 1'; + + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputServerName@@@', @OutputServerName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputTableName@@@', @OutputTableName); -RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET count_included_columns = D4.count_included_columns, - count_key_columns = D4.count_key_columns -FROM #IndexSanity si - CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 - ELSE 0 - END) AS count_included_columns, - SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 - ELSE 0 - END) AS count_key_columns - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - ) AS D4 ( count_included_columns, count_key_columns ); + END -RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; -UPDATE #IndexPartitionSanity -SET index_sanity_id = i.index_sanity_id -FROM #IndexPartitionSanity ps - JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] - AND ps.index_id = i.index_id - AND i.database_id = ps.database_id - AND i.schema_name = ps.schema_name -RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; -INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], partition_count, total_rows, total_reserved_MB, - total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_range_scan_count, - total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, - total_forwarded_fetch_count,total_row_lock_count, - total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, - total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, - avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, - total_index_lock_promotion_count, data_compression_desc ) - SELECT index_sanity_id, ipp.database_id, ipp.schema_name, - COUNT(*), SUM(row_count), SUM(reserved_MB), SUM(reserved_LOB_MB), - SUM(reserved_row_overflow_MB), - SUM(range_scan_count), - SUM(singleton_lookup_count), - SUM(leaf_delete_count), - SUM(leaf_update_count), - SUM(forwarded_fetch_count), - SUM(row_lock_count), - SUM(row_lock_wait_count), - SUM(row_lock_wait_in_ms), - CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN - SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) - ELSE 0 END AS avg_row_lock_wait_in_ms, - SUM(page_lock_count), - SUM(page_lock_wait_count), - SUM(page_lock_wait_in_ms), - CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN - SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) - ELSE 0 END AS avg_page_lock_wait_in_ms, - SUM(index_lock_promotion_attempt_count), - SUM(index_lock_promotion_count), - LEFT(MAX(data_compression_info.data_compression_rollup),8000) - FROM #IndexPartitionSanity ipp - /* individual partitions can have distinct compression settings, just roll them into a list here*/ - OUTER APPLY (SELECT STUFF(( - SELECT N', ' + data_compression_desc - FROM #IndexPartitionSanity ipp2 - WHERE ipp.[object_id]=ipp2.[object_id] - AND ipp.[index_id]=ipp2.[index_id] - AND ipp.database_id = ipp2.database_id - AND ipp.schema_name = ipp2.schema_name - ORDER BY ipp2.partition_number - FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '')) - data_compression_info(data_compression_rollup) - GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name - ORDER BY index_sanity_id -OPTION ( RECOMPILE ); -RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; -UPDATE #MissingIndexes -SET is_low = CASE WHEN (user_seeks + user_scans) < 10000 - OR avg_user_impact < 70. THEN 1 - ELSE 0 - END; + IF @Mode IN (0, 4) /* DIAGNOSE */ + BEGIN; + IF @Mode IN (0, 4) /* DIAGNOSE priorities 1-100 */ + BEGIN; + RAISERROR(N'@Mode=0 or 4, running rules for priorities 1-100.', 0,1) WITH NOWAIT; -RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; -UPDATE #IndexSanity - SET is_referenced_by_foreign_key=1 -FROM #IndexSanity s -JOIN #ForeignKeys fk ON - s.object_id=fk.referenced_object_id - AND s.database_id=fk.database_id - AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns + ---------------------------------------- + --Multiple Index Personalities: Check_id 0-10 + ---------------------------------------- + RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; + WITH duplicate_indexes + AS ( SELECT [object_id], key_column_names, database_id, [schema_name] + FROM #IndexSanity AS ip + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical = 0 + AND is_disabled = 0 + AND is_primary_key = 0 + AND EXISTS ( + SELECT 1/0 + FROM #IndexSanitySize ips + WHERE ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name + AND ips.total_reserved_MB >= CASE + WHEN (@GetAllDatabases = 1 OR @Mode = 0) + THEN @ThresholdMB + ELSE ips.total_reserved_MB + END + ) + GROUP BY [object_id], key_column_names, database_id, [schema_name] + HAVING COUNT(*) > 1) + 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 1 AS check_id, + ip.index_sanity_id, + 20 AS Priority, + 'Redundant Indexes' AS findings_group, + 'Duplicate keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/duplicateindex' AS URL, + N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM duplicate_indexes di + JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] + AND ip.database_id = di.database_id + AND ip.[schema_name] = di.[schema_name] + AND di.key_column_names = ip.key_column_names + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name + /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ + WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END + AND ip.is_primary_key = 0 + ORDER BY ips.total_rows DESC, ip.[schema_name], ip.[object_name], ip.key_column_names_with_sort_order + OPTION ( RECOMPILE ); -RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; -UPDATE nc -SET secret_columns= - N'[' + - CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS VARCHAR(10)) END + - CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + - CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + - CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + - /* Uniquifiers only needed on non-unique clustereds-- not heaps */ - CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END - END - , count_secret_columns= - CASE tb.index_id WHEN 0 THEN 1 ELSE - tb.count_key_columns + - CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END - END -FROM #IndexSanity AS nc -JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id - AND nc.database_id = tb.database_id - AND nc.schema_name = tb.schema_name - AND tb.index_id IN (0,1) -WHERE nc.index_id > 1; + RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; + WITH borderline_duplicate_indexes + AS ( SELECT DISTINCT database_id, [object_id], first_key_column_name, key_column_names, + COUNT([object_id]) OVER ( PARTITION BY database_id, [object_id], first_key_column_name ) AS number_dupes + FROM #IndexSanity + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical=0 + AND is_disabled=0 + AND is_primary_key = 0) + 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 2 AS check_id, + ip.index_sanity_id, + 30 AS Priority, + 'Redundant Indexes' AS findings_group, + 'Approximate Duplicate Keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/duplicateindex' AS URL, + ip.db_schema_object_indexid AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM #IndexSanity AS ip + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + WHERE EXISTS ( + SELECT di.[object_id] + FROM borderline_duplicate_indexes AS di + WHERE di.[object_id] = ip.[object_id] AND + di.database_id = ip.database_id AND + di.first_key_column_name = ip.first_key_column_name AND + di.key_column_names <> ip.key_column_names AND + di.number_dupes > 1 + ) + AND ip.is_primary_key = 0 + 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 ); -RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; -UPDATE tb -SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END - , count_secret_columns = 1 -FROM #IndexSanity AS tb -WHERE tb.index_id = 0 /*Heaps-- these have the RID */ - OR (tb.index_id=1 AND tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ + ---------------------------------------- + --Aggressive Indexes: Check_id 10-19 + ---------------------------------------- + RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page)', 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 11 AS check_id, + i.index_sanity_id, + 70 AS Priority, + N'Locking-Prone ' + + CASE COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) + WHEN 0 THEN N'Under-Indexing' + WHEN 1 THEN N'Under-Indexing' + WHEN 2 THEN N'Under-Indexing' + WHEN 3 THEN N'Under-Indexing' + WHEN 4 THEN N'Indexes' + WHEN 5 THEN N'Indexes' + WHEN 6 THEN N'Indexes' + WHEN 7 THEN N'Indexes' + WHEN 8 THEN N'Indexes' + WHEN 9 THEN N'Indexes' + ELSE N'Over-Indexing' + END AS findings_group, + N'Total lock wait time > 5 minutes (row + page)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AggressiveIndexes' AS URL, + (i.db_schema_object_indexid + N': ' + + sz.index_lock_wait_summary + N' NC indexes on table: ') COLLATE DATABASE_DEFAULT + + CAST(COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) + AS NVARCHAR(30)) AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 + GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id + ORDER BY SUM(total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) DESC, 4, [database_name], 8 + OPTION ( RECOMPILE ); -RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; -INSERT #IndexCreateTsql (index_sanity_id, create_tsql) -SELECT - index_sanity_id, - ISNULL ( - /* Script drops for disabled non-clustered indexes*/ - CASE WHEN is_disabled = 1 AND index_id <> 1 - THEN N'--DROP INDEX ' + QUOTENAME([index_name]) + N' ON ' - + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) - ELSE - CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' - ELSE - CASE WHEN is_XML = 1 OR is_spatial=1 THEN N'' /* Not even trying for these just yet...*/ - ELSE - CASE WHEN is_primary_key=1 THEN - N'ALTER TABLE ' + QUOTENAME([schema_name]) + - N'.' + QUOTENAME([object_name]) + - N' ADD CONSTRAINT [' + - index_name + - N'] PRIMARY KEY ' + - CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + - key_column_names_with_sort_order_no_types + N' )' - WHEN is_CX_columnstore= 1 THEN - N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) - ELSE /*Else not a PK or cx columnstore */ - N'CREATE ' + - CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + - CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + - CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' - ELSE N'' END + - N'INDEX [' - + index_name + N'] ON ' + - QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + - CASE WHEN is_NC_columnstore=1 THEN - N' (' + ISNULL(include_column_names_no_types,'') + N' )' - ELSE /*Else not colunnstore */ - N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' - + CASE WHEN include_column_names_no_types IS NOT NULL THEN - N' INCLUDE (' + include_column_names_no_types + N')' - ELSE N'' - END - END /*End non-colunnstore case */ - + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END - END /*End Non-PK index CASE */ - + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN - N' WITH (' - + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' - + N'ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - ELSE N'' END - + N';' - END /*End non-spatial and non-xml CASE */ - END - END, '[Unknown Error]') - AS create_tsql -FROM #IndexSanity; - -RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; -;WITH [maps] - AS ( SELECT - index_sanity_id, - partition_number, - data_compression_desc, - partition_number - ROW_NUMBER() OVER (PARTITION BY ips.index_sanity_id, data_compression_desc ORDER BY partition_number ) AS [rN] - FROM #IndexPartitionSanity ips - ), - [grps] - AS ( SELECT MIN([maps].[partition_number]) AS [MinKey] , - MAX([maps].[partition_number]) AS [MaxKey] , - index_sanity_id, - maps.data_compression_desc - FROM [maps] - GROUP BY [maps].[rN], index_sanity_id, maps.data_compression_desc) -INSERT #PartitionCompressionInfo - (index_sanity_id, partition_compression_detail) -SELECT DISTINCT grps.index_sanity_id , SUBSTRING(( STUFF((SELECT ', ' + ' Partition' - + CASE WHEN [grps2].[MinKey] < [grps2].[MaxKey] - THEN +'s ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' - ' - + CAST([grps2].[MaxKey] AS VARCHAR) - + ' use ' + grps2.data_compression_desc - ELSE ' ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' uses ' + grps2.data_compression_desc - END AS [Partitions] - FROM [grps] AS grps2 - WHERE grps2.index_sanity_id = grps.index_sanity_id - ORDER BY grps2.MinKey, grps2.MaxKey - FOR XML PATH('') , - TYPE - ).[value]('.', 'VARCHAR(MAX)'), 1, 1, '') ), 0, 8000) AS [partition_compression_detail] -FROM grps; - -RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; -UPDATE sz -SET sz.data_compression_desc = pci.partition_compression_detail -FROM #IndexSanitySize sz -JOIN #PartitionCompressionInfo AS pci -ON pci.index_sanity_id = sz.index_sanity_id; + ---------------------------------------- + --Index Hoarder: Check_id 20-29 + ---------------------------------------- + RAISERROR(N'check_id 20: >= 10 NC indexes on any given table. Yes, 10 is an arbitrary number.', 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 20 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + 10 AS Priority, + 'Over-Indexing' AS findings_group, + 'Many NC Indexes on a Single Table' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, + i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, + '' AS secret_columns, + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS MONEY), 1), N'.00', N'') + N' reads (ALL); ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS MONEY), 1), N'.00', N'') + N' writes (ALL); ', + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS MONEY), 1), N'.00', N'') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id NOT IN ( 0, 1 ) + GROUP BY db_schema_object_name, [i].[database_name] + HAVING COUNT(*) >= 10 + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); -/*This is for debugging*/ ---SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; ---SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; ---SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; ---SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; ---SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; ---SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; ---SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults; ---SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; ---SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; ---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; -/*End debug*/ + RAISERROR(N'check_id 22: NC indexes with 0 reads and >= 10,000 writes', 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 22 AS check_id, + i.index_sanity_id, + 10 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC Index with High Writes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Reads: 0,' + + N' Writes: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + + N' on: ' + + i.db_schema_object_indexid + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.user_updates >= 10000 + 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 + AND @Filter <> 1 /* 1 = "ignore unused */ + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); ----------------------------------------- ---STEP 3: DIAGNOSE THE PATIENT ----------------------------------------- + RAISERROR(N'check_id 34: Filtered index definition columns not in index definition', 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 34 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Filter Columns Not In Index Definition' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'The index ' + + QUOTENAME(i.index_name) + + N' on [' + + i.db_schema_object_name + + N'] has a filter on [' + + i.filter_definition + + N'] but is missing [' + + LTRIM(i.filter_columns_not_in_index) + + N'] from the index definition.' + 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 + 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 ); -BEGIN TRY ----------------------------------------- ---If @TableName is specified, just return information for that table. ---The @Mode parameter doesn't matter if you're looking at a specific table. ----------------------------------------- -IF @TableName IS NOT NULL -BEGIN - RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; + ---------------------------------------- + --Self Loathing Indexes : Check_id 40-49 + ---------------------------------------- + + RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Nonclustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ + CASE WHEN (last_user_update IS NULL OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id > 1 + AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - --We do a left join here in case this is a disabled NC. - --In that case, it won't have any size info/pages allocated. - - - WITH table_mode_cte AS ( - SELECT - s.db_schema_object_indexid, - s.key_column_names, - s.index_definition, - ISNULL(s.secret_columns,N'') AS secret_columns, - s.fill_factor, - s.index_usage_summary, - sz.index_op_stats, - ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, - partition_compression_detail , - ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, - s.is_referenced_by_foreign_key, - (SELECT COUNT(*) - FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id - AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, - s.last_user_seek, - s.last_user_scan, - s.last_user_lookup, - s.last_user_update, - s.create_date, - s.modify_date, - ct.create_tsql, - 1 AS display_order - FROM #IndexSanity s - LEFT JOIN #IndexSanitySize sz ON - s.index_sanity_id=sz.index_sanity_id - LEFT JOIN #IndexCreateTsql ct ON - s.index_sanity_id=ct.index_sanity_id - LEFT JOIN #PartitionCompressionInfo pci ON - pci.index_sanity_id = s.index_sanity_id - WHERE s.[object_id]=@ObjectID - UNION ALL - SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + - N' (' + @ScriptVersionName + ')' , - N'SQL Server First Responder Kit' , - N'http://FirstResponderKit.org' , - N'From Your Community Volunteers', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - 0 AS display_order - ) - SELECT - db_schema_object_indexid AS [Details: db_schema.table.index(indexid)], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - secret_columns AS [Secret Columns], - fill_factor AS [Fillfactor], - index_usage_summary AS [Usage Stats], - index_op_stats AS [Op Stats], - index_size_summary AS [Size], - partition_compression_detail AS [Compression Type], - index_lock_wait_summary AS [Lock Waits], - is_referenced_by_foreign_key AS [Referenced by FK?], - FKs_covered_by_index AS [FK Covered by Index?], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Write], - create_date AS [Created], - modify_date AS [Last Modified], - create_tsql AS [Create TSQL] - FROM table_mode_cte - ORDER BY display_order ASC, key_column_names ASC - OPTION ( RECOMPILE ); + RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Clustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ + CASE WHEN (last_user_update IS NULL OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id = 1 + AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 43: Heaps with forwarded records', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0) + 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 43 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Forwarded Fetches' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CASE WHEN h.forwarded_fetch_count >= 922337203685477 THEN '>= 922,337,203,685,477' + WHEN @DaysUptime < 1 THEN CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + N' forwarded fetches against heap: ' + db_schema_object_indexid + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (h.forwarded_fetch_count /*/@DaysUptime */) + AS BIGINT) AS MONEY), 1), '.00', '') + END + N' forwarded fetches per day against heap: ' + + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND h.forwarded_fetch_count / @DaysUptime > 1000 + AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END + OPTION ( RECOMPILE ); - IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL - BEGIN + RAISERROR(N'check_id 44: Large Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 44 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Large Active Heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows >= 100000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT N'Missing index.' AS Finding , - N'http://BrentOzar.com/go/Indexaphobia' AS URL , - mi.[statement] + - ' Est. Benefit: ' - + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS [Estimated Benefit], - missing_index_details AS [Missing Index Request] , - index_estimated_impact AS [Estimated Impact], - create_tsql AS [Create TSQL] - FROM #MissingIndexes mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - WHERE mi.[object_id] = @ObjectID - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - AND (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - ORDER BY is_low, magic_benefit_number DESC - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No missing indexes.' AS finding; + RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 45 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Medium Active heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND + (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows >= 10000 AND sz.total_rows < 100000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); - SELECT - column_name AS [Column Name], - (SELECT COUNT(*) - FROM #IndexColumns c2 - WHERE c2.column_name=c.column_name - AND c2.key_ordinal IS NOT NULL) - + CASE WHEN c.index_id = 1 AND c.key_ordinal IS NOT NULL THEN - -1+ (SELECT COUNT(DISTINCT index_id) - FROM #IndexColumns c3 - WHERE c3.index_id NOT IN (0,1)) - ELSE 0 END - AS [Found In], - system_type_name + - CASE max_length WHEN -1 THEN N' (max)' ELSE - CASE - WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' - WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' - ELSE '' - END - END - AS [Type], - CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], - max_length AS [Length (max bytes)], - [precision] AS [Prec], - [scale] AS [Scale], - CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], - CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], - CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], - CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], - CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], - collation_name AS [Collation] - FROM #IndexColumns AS c - WHERE index_id IN (0,1); + RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 46 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Small Active heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND + (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows < 10000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); - IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL - BEGIN - SELECT [database_name] + N':' + parent_object_name + N': ' + foreign_key_name AS [Foreign Key], - parent_fk_columns AS [Foreign Key Columns], - referenced_object_name AS [Referenced Table], - referenced_fk_columns AS [Referenced Table Columns], - is_disabled AS [Is Disabled?], - is_not_trusted AS [Not Trusted?], - is_not_for_replication [Not for Replication?], - [update_referential_action_desc] AS [Cascading Updates?], - [delete_referential_action_desc] AS [Cascading Deletes?] - FROM #ForeignKeys - ORDER BY [Foreign Key] - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No foreign keys.' AS finding; -END + RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 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 47 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heap with a Nonclustered Primary Key' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' 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 + WHERE i.index_type = 2 AND i.is_primary_key = 1 + AND EXISTS + ( + SELECT 1/0 + FROM #IndexSanity AS isa + WHERE i.database_id = isa.database_id + AND i.object_id = isa.object_id + AND isa.index_id = 0 + ) + OPTION ( RECOMPILE ); ---If @TableName is NOT specified... ---Act based on the @Mode and @Filter. (@Filter applies only when @Mode=0 "diagnose") -ELSE -BEGIN; - IF @Mode IN (0, 4) /* DIAGNOSE*/ - BEGIN; - RAISERROR(N'@Mode=0 or 4, we are diagnosing.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 48: Nonclustered indexes with a bad read to write ratio', 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 48 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Over-Indexing' AS findings_group, + N'NC index with High Writes:Reads' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.total_reads) AS MONEY), 1), N'.00', N'') + + N' Writes: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + + N' on: ' + + i.db_schema_object_indexid 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 + WHERE i.total_reads > 0 /*Not totally unused*/ + AND i.user_updates >= 10000 /*Decent write activity*/ + AND i.total_reads < 10000 + AND ((i.total_reads * 10) < i.user_updates) /*10x more writes than reads*/ + 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 + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); ---------------------------------------- - --Multiple Index Personalities: Check_id 0-10 + --Indexaphobia + --Missing indexes with value >= 5 million: : Check_id 50-59 ---------------------------------------- - BEGIN; + RAISERROR(N'check_id 50: High Value Missing Index.', 0,1) WITH NOWAIT; + WITH index_size_cte + AS ( SELECT i.database_id, + i.schema_name, + i.[object_id], + MAX(i.index_sanity_id) AS index_sanity_id, + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days, + ISNULL ( + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) + AS NVARCHAR(30))+ N' NC indexes exist (' + + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 + THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. + + AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' + ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + AS NVARCHAR(30)) + N'MB); ' + END + + CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS MONEY), 1), '.00', '') + END + + + N' Estimated Rows;' + ,N'') AS index_size_summary + FROM #IndexSanity AS i + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id AND i.database_id = sz.database_id + WHERE i.is_hypothetical = 0 + AND i.is_disabled = 0 + GROUP BY i.database_id, i.schema_name, i.[object_id]) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + index_usage_summary, index_size_summary, create_tsql, more_info, sample_query_plan ) + + SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], + index_estimated_impact, t.index_size_summary, create_tsql, more_info, sample_query_plan + FROM + ( + SELECT ROW_NUMBER() OVER (ORDER BY magic_benefit_number DESC) AS rownum, + 50 AS check_id, + sz.index_sanity_id, + 40 AS Priority, + N'Index Suggestion' AS findings_group, + N'High Value Missing Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/Indexaphobia' AS URL, + mi.[statement] + + N' Est. benefit per day: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (magic_benefit_number/@DaysUptime) + AS BIGINT) AS MONEY), 1), '.00', '') + END AS details, + missing_index_details AS [definition], + index_estimated_impact, + sz.index_size_summary, + mi.create_tsql, + mi.more_info, + magic_benefit_number, + mi.is_low, + mi.sample_query_plan + FROM #MissingIndexes mi + LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id + AND mi.database_id = sz.database_id + AND mi.schema_name = sz.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) + OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 + ) AS t + WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); - --SELECT [object_id], key_column_names, database_id - -- FROM #IndexSanity - -- WHERE index_type IN (1,2) /* Clustered, NC only*/ - -- AND is_hypothetical = 0 - -- AND is_disabled = 0 - -- GROUP BY [object_id], key_column_names, database_id - -- HAVING COUNT(*) > 1 - RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; - WITH duplicate_indexes - AS ( SELECT [object_id], key_column_names, database_id, [schema_name] - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical = 0 - AND is_disabled = 0 - AND is_primary_key = 0 - GROUP BY [object_id], key_column_names, database_id, [schema_name] - HAVING COUNT(*) > 1) - 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 1 AS check_id, - ip.index_sanity_id, - 50 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Duplicate keys' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM duplicate_indexes di - JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] - AND ip.database_id = di.database_id - AND ip.[schema_name] = di.[schema_name] - AND di.key_column_names = ip.key_column_names - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id AND ip.database_id = ips.database_id - /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ - WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END - AND ip.is_primary_key = 0 - ORDER BY ip.object_id, ip.key_column_names_with_sort_order - OPTION ( RECOMPILE ); + RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; + -- Allowed Ranges: + --int -2,147,483,648 to 2,147,483,647 + --smallint -32,768 to 32,768 + --tinyint 0 to 255 - RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; - WITH borderline_duplicate_indexes - AS ( SELECT DISTINCT database_id, [object_id], first_key_column_name, key_column_names, - COUNT([object_id]) OVER ( PARTITION BY database_id, [object_id], first_key_column_name ) AS number_dupes - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical=0 - AND is_disabled=0 - AND is_primary_key = 0) 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 2 AS check_id, - ip.index_sanity_id, - 60 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Borderline duplicate keys' AS finding, + SELECT 68 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Within ' + + CAST (calc1.percent_remaining AS NVARCHAR(256)) + + N' Percent End of Range' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - ip.db_schema_object_indexid AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM #IndexSanity AS ip - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id - WHERE EXISTS ( - SELECT di.[object_id] - FROM borderline_duplicate_indexes AS di - WHERE di.[object_id] = ip.[object_id] AND - di.database_id = ip.database_id AND - di.first_key_column_name = ip.first_key_column_name AND - di.key_column_names <> ip.key_column_names AND - di.number_dupes > 1 - ) - AND ip.is_primary_key = 0 - /* WHERE clause skips near-duplicate indexes when getting all databases or using PainRelief mode */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - ORDER BY ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names - OPTION ( RECOMPILE ); + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + CROSS APPLY ( + SELECT CAST(CASE WHEN ic.increment_value >= 0 + THEN + CASE ic.system_type_name + WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE 999 + END + ELSE --ic.increment_value is negative + CASE ic.system_type_name + WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE -1 + END + END AS NUMERIC(5,1)) AS percent_remaining + ) AS calc1 + WHERE i.index_id IN (1,0) + AND calc1.percent_remaining <= 30 + OPTION (RECOMPILE); - END - ---------------------------------------- - --Aggressive Indexes: Check_id 10-19 - ---------------------------------------- - BEGIN; - RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page) with long average waits', 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 11 AS check_id, + RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; + IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) + AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) + BEGIN + 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 72 AS check_id, i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with long average waits' AS finding, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + 'Columnstore Indexes with Trace Flag 834' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, + N'https://support.microsoft.com/en-us/kb/3210239' AS URL, + i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, i.index_usage_summary, - sz.index_size_summary + ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) > 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_type IN (5,6) OPTION ( RECOMPILE ); + END; - RAISERROR(N'check_id 12: Total lock wait time > 5 minutes (row + page) with short average waits', 0,1) WITH NOWAIT; - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + ---------------------------------------- + --Statistics Info: Check_id 90-99, as well as 125 + ---------------------------------------- + + RAISERROR(N'check_id 90: Outdated 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 ) - SELECT 12 AS check_id, - i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with short average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) < 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id - OPTION ( RECOMPILE ); + SELECT 90 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics Not Updated Recently', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Statistics on this table were last updated ' + + CASE WHEN s.last_statistics_update IS NULL THEN N' NEVER ' + ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + + ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + + ' modifications in that time, which is ' + + CONVERT(NVARCHAR(100), s.percent_modifications) + + '% of the table.' + END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 30) + AND s.percent_modifications >= 10. + AND s.rows >= 10000 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Low Sampling Rates', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows BETWEEN 10000 AND 1000000 AND s.percent_sampled < 10) + 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 ) + SELECT 92 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics With NO RECOMPUTE', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.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 ) + SELECT 94 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Check Constraint with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/computedscalar' AS URL, + 'The check constraint ' + QUOTENAME(cc.constraint_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #CheckConstraints AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 99: Computed Columns 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 ) + SELECT 99 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Computed Column with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/serialudf' AS URL, + 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #ComputedColumns AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + + + END /* IF @Mode IN (0, 4) DIAGNOSE priorities 1-100 */ + + - END - ---------------------------------------- - --Index Hoarder: Check_id 20-29 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 20: >=7 NC indexes on any given table. Yes, 7 is an arbitrary number.', 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 20 AS check_id, + + + + + IF @Mode = 4 /* DIAGNOSE*/ + BEGIN; + RAISERROR(N'@Mode=4, running rules for priorities 101+.', 0,1) WITH NOWAIT; + + RAISERROR(N'check_id 21: More Than 5 Percent NC Indexes Are Unused', 0,1) WITH NOWAIT; + DECLARE @percent_NC_indexes_unused NUMERIC(29,1); + DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); + + SELECT @percent_NC_indexes_unused = ( 100.00 * SUM(CASE + WHEN total_reads = 0 + THEN 1 + ELSE 0 + END) ) / COUNT(*), + @NC_indexes_unused_reserved_MB = SUM(CASE + WHEN total_reads = 0 + THEN sz.total_reserved_MB + ELSE 0 + END) + FROM #IndexSanity i + 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()) + 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, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 21 AS check_id, MAX(i.index_sanity_id) AS index_sanity_id, - 100 AS Priority, - 'Index Hoarder' AS findings_group, - 'Many NC indexes on a single table' AS finding, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'More Than 5 Percent NC Indexes Are Unused' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, - i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, - '' AS secret_columns, - REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS MONEY), 1), N'.00', N'') + N' reads (ALL); ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS MONEY), 1), N'.00', N'') + N' writes (ALL); ', - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS MONEY), 1), N'.00', N'') + N' rows (MAX)' + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + + N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, + i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, + '' AS secret_columns, + CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' + + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, + + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' + CASE WHEN SUM(total_reserved_MB) > 1024 THEN N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' WHEN SUM(total_reserved_MB) > 0 THEN @@ -23333,110 +25316,26 @@ BEGIN; ELSE '' END AS index_size_summary FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id NOT IN ( 0, 1 ) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - GROUP BY db_schema_object_name, [i].[database_name] - HAVING COUNT(*) >= 7 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - IF @Filter = 1 /*@Filter=1 is "ignore unusued" */ - BEGIN - RAISERROR(N'Skipping checks on unused indexes (21 and 22) because @Filter=1', 0,1) WITH NOWAIT; - END - ELSE /*Otherwise, go ahead and do the checks*/ - BEGIN - RAISERROR(N'check_id 21: >=5 percent of indexes are unused. Yes, 5 is an arbitrary number.', 0,1) WITH NOWAIT; - DECLARE @percent_NC_indexes_unused NUMERIC(29,1); - DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); - - SELECT @percent_NC_indexes_unused =( 100.00 * SUM(CASE WHEN total_reads = 0 THEN 1 - ELSE 0 - END) ) / COUNT(*) , - @NC_indexes_unused_reserved_MB = SUM(CASE WHEN total_reads = 0 THEN sz.total_reserved_MB - ELSE 0 - END) - FROM #IndexSanity i - 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()) - 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, - secret_columns, index_usage_summary, index_size_summary ) - SELECT 21 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'More than 5 percent NC indexes are unused' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + - N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, - i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, - '' AS secret_columns, - CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' - + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, - - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND i.is_unique = 0 - AND total_reads = 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 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()) - GROUP BY i.database_name - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 22: NC indexes with 0 reads. (Borderline) and >= 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with High Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates >= 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); - END /*end checks only run when @Filter <> 1*/ + 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()) + GROUP BY i.database_name + OPTION ( RECOMPILE ); RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, + SELECT 23 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Borderline: Wide indexes (7 or more columns)' AS finding, + N'Over-Indexing' AS findings_group, + N'Approximate: Wide Indexes (7 or More Columns)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' + i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -23445,7 +25344,6 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id WHERE ( count_key_columns + count_included_columns ) >= 7 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; @@ -23459,20 +25357,24 @@ BEGIN; ) 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 24 AS check_id, + SELECT 24 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide clustered index (> 3 columns OR > 16 bytes)' AS finding, + N'Over-Indexing' AS findings_group, + N'Wide Clustered Index (> 3 columns OR > 16 bytes)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' + CAST(cc.sum_max_length AS NVARCHAR(10)) + N' bytes in clustered index:' + i.db_schema_object_name + N'. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + N' NC indexes on the table.' AS details, i.index_definition, @@ -23484,14 +25386,13 @@ BEGIN; JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] AND i.database_id = cc.database_id WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND (count_key_columns > 3 /*More than three key columns.*/ OR cc.sum_max_length > 16 /*More than 16 bytes in key */) AND i.is_CX_columnstore = 0 ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - RAISERROR(N'check_id 25: Addicted to nullable columns.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 25: High ratio of nullable columns.', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], [database_id], @@ -23506,13 +25407,13 @@ BEGIN; ) 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 25 AS check_id, + SELECT 25 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to nulls' AS finding, + N'Over-Indexing' AS findings_group, + N'High Ratio of Nulls' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, i.db_schema_object_name + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) + N' of ' + CAST(total_columns AS NVARCHAR(10)) @@ -23527,7 +25428,6 @@ BEGIN; AND cc.database_id = ip.database_id AND cc.[schema_name] = ip.[schema_name] WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND cc.non_nullable_columns < 2 AND cc.total_columns > 3 ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); @@ -23548,13 +25448,13 @@ BEGIN; ) 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 26 AS check_id, + SELECT 26 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide tables: 35+ cols or > 2000 non-LOB bytes' AS finding, + N'Over-Indexing' AS findings_group, + N'Wide Tables: 35+ cols or > 2000 non-LOB bytes' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, i.db_schema_object_name + N' has ' + CAST((total_columns) AS NVARCHAR(10)) + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) @@ -23573,13 +25473,12 @@ BEGIN; AND cc.database_id = i.database_id AND cc.[schema_name] = i.[schema_name] WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND (cc.total_columns >= 35 OR cc.sum_max_length >= 2000) ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - RAISERROR(N'check_id 27: Addicted to strings.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 27: High Ratio of Strings.', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], [database_id], @@ -23594,13 +25493,13 @@ BEGIN; ) 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 27 AS check_id, + SELECT 27 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to strings' AS finding, + N'Over-Indexing' AS findings_group, + N'High Ratio of Strings' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, i.db_schema_object_name + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) + N' of ' + CAST(total_columns AS NVARCHAR(10)) @@ -23616,7 +25515,6 @@ BEGIN; AND cc.[schema_name] = i.[schema_name] CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND calc1.non_string_or_lob_columns <= 1 AND cc.total_columns > 3 ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); @@ -23624,18 +25522,22 @@ BEGIN; RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, + SELECT 28 AS check_id, i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Non-Unique clustered index' AS finding, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Non-Unique Clustered Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name + N' and all NC indexes. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + N' NC indexes on the table.' AS details, i.index_definition, @@ -23645,63 +25547,66 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND is_unique=0 /* not unique */ AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ) + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - RAISERROR(N'check_id 29: NC indexes with 0 reads. (Borderline) and < 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with Low Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates < 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); + RAISERROR(N'check_id 29: NC indexes with 0 reads and < 10,000 writes', 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 29 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC index with Low Writes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'0 reads: ' + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.user_updates < 10000 + 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()) + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); - END - ---------------------------------------- + + ---------------------------------------- --Feature-Phobic Indexes: Check_id 30-39 ---------------------------------------- - BEGIN RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 */ - SELECT database_name, - SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, - 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes + SELECT database_name, + SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, + 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes INTO #index_includes FROM #IndexSanity + WHERE is_hypothetical = 0 + AND is_disabled = 0 + AND NOT (@GetAllDatabases = 1 OR @Mode = 0) GROUP BY database_name; - IF NOT (@Mode = 0) 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 30 AS check_id, NULL AS index_sanity_id, 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, + N'Omitted Index Features' AS findings_group, database_name AS [Database Name], - N'No indexes use includes' AS finding, 'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'No indexes use includes' AS details, + N'No Indexes Use Includes' AS finding, 'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'No Indexes Use Includes' AS details, database_name + N' (Entire database)' AS index_definition, N'' AS secret_columns, N'N/A' AS index_usage_summary, @@ -23711,157 +25616,90 @@ BEGIN; OPTION ( RECOMPILE ); RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 0,1) WITH NOWAIT; - IF NOT (@Mode = 0) 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 31 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: Includes are used in < 3% of indexes' AS findings, - database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, - N'Entire database' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 - OPTION ( RECOMPILE ); + SELECT 31 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Few Indexes Use Includes' AS findings, + database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #index_includes + WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 + OPTION ( RECOMPILE ); RAISERROR(N'check_id 32: filtered indexes and indexed views', 0,1) WITH NOWAIT; - IF NOT (@Mode = 0) 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 DISTINCT - 32 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: No filtered indexes or indexed views exist' AS finding, - i.database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'These are NOT always needed-- but do you know when you would use them?' AS details, - i.database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #IndexSanity i - WHERE i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE filter_definition <> '' ) - AND i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE is_indexed_view = 1 ) - OPTION ( RECOMPILE ); - END; + SELECT DISTINCT + 32 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'No Filtered Indexes or Indexed Views' AS finding, + i.database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'These are NOT always needed-- but do you know when you would use them?' AS details, + i.database_name + N' (Entire database)' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #IndexSanity i + WHERE i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE filter_definition <> '' ) + AND i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE is_indexed_view = 1 ) + OPTION ( RECOMPILE ); RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, - i.index_sanity_id AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Potential filtered index (based on column name)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexColumns ic - JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] - AND ic.database_id =i.database_id - AND ic.schema_name = i.schema_name - AND ic.[index_id]=i.[index_id] - AND i.[index_id] > 1 /* non-clustered index */ - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (column_name LIKE 'is%' - OR column_name LIKE '%archive%' - OR column_name LIKE '%active%' - OR column_name LIKE '%flag%') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - ---------------------------------------- - --Self Loathing Indexes : Check_id 40-49 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: nonclustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id > 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - + SELECT 33 AS check_id, + i.index_sanity_id AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Potential Filtered Index (Based on Column Name)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexColumns ic + JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] + AND ic.database_id =i.database_id + AND ic.schema_name = i.schema_name + AND ic.[index_id]=i.[index_id] + AND i.[index_id] > 1 /* non-clustered index */ + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (column_name LIKE 'is%' + OR column_name LIKE '%archive%' + OR column_name LIKE '%active%' + OR column_name LIKE '%flag%') + OPTION ( RECOMPILE ); RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, + SELECT 41 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, + N'Indexes Worth Reviewing' AS findings_group, N'Hypothetical Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Hypothetical Index: ' + db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -23869,7 +25707,6 @@ BEGIN; N'' AS index_size_summary FROM #IndexSanity AS i WHERE is_hypothetical = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); @@ -23877,13 +25714,13 @@ BEGIN; --Note: disabled NC indexes will have O rows in #IndexSanitySize! 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 42 AS check_id, + SELECT 42 AS check_id, index_sanity_id, 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, + N'Indexes Worth Reviewing' AS findings_group, N'Disabled Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Disabled Index:' + db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -23891,280 +25728,57 @@ BEGIN; 'DISABLED' AS index_size_summary FROM #IndexSanity AS i WHERE is_disabled = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); - RAISERROR(N'check_id 43: Heaps with forwarded records or deletes', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 43 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heaps with forwarded records or deletes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + ' forwarded fetches, ' - + CAST(h.leaf_delete_count AS NVARCHAR(256)) + ' deletes against heap:' - + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 44: Large Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 44 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Large Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 45 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Medium Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 10000 AND sz.total_rows < 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 49: Heaps with deletes', 0,1) WITH NOWAIT; WITH heaps_cte AS ( SELECT [object_id], [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, + [schema_name], SUM(leaf_delete_count) AS leaf_delete_count FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 46 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Small Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows < 10000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 0,1) WITH NOWAIT; + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) < 1000 * @DaysUptime /* Only alert about indexes with no forwarded fetches - we already alerted about those in check_id 43 */ + AND SUM(leaf_delete_count) > 0) 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 47 AS check_id, + SELECT 49 AS check_id, i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heap with a Nonclustered Primary Key' AS finding, + 200 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Deletes' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' AS details, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CAST(h.leaf_delete_count AS NVARCHAR(256)) + N' deletes against heap:' + + db_schema_object_indexid AS details, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type = 2 AND i.is_primary_key = 1 AND i.secret_columns LIKE '%RID%' + WHERE i.index_id = 0 + AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END OPTION ( RECOMPILE ); - END; - ---------------------------------------- - --Indexaphobia - --Missing indexes with value >= 5 million: : Check_id 50-59 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 50: Indexaphobia.', 0,1) WITH NOWAIT; - WITH index_size_cte - AS ( SELECT i.database_id, - i.schema_name, - i.[object_id], - MAX(i.index_sanity_id) AS index_sanity_id, - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days, - ISNULL ( - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) - AS NVARCHAR(30))+ N' NC indexes exist (' + - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 - THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. - - AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' - ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - AS NVARCHAR(30)) + N'MB); ' - END + - CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS MONEY), 1), '.00', '') - END + - + N' Estimated Rows;' - ,N'') AS index_size_summary - FROM #IndexSanity AS i - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id AND i.database_id = sz.database_id - WHERE i.is_hypothetical = 0 - AND i.is_disabled = 0 - GROUP BY i.database_id, i.schema_name, i.[object_id]) - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, - index_usage_summary, index_size_summary, create_tsql, more_info ) - - SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], - index_estimated_impact, t.index_size_summary, create_tsql, more_info - FROM - ( - SELECT ROW_NUMBER() OVER (ORDER BY mi.is_low, magic_benefit_number DESC) AS rownum, - 50 AS check_id, - sz.index_sanity_id, - 10 AS Priority, - N'Indexaphobia' AS findings_group, - N'High value missing index' + CASE mi.is_low - WHEN 0 THEN N' with High Impact' - WHEN 1 THEN N' with Low Impact' - END - AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Indexaphobia' AS URL, - mi.[statement] + - N' Est. benefit per day: ' + - CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number/@DaysUptime) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS details, - missing_index_details AS [definition], - index_estimated_impact, - sz.index_size_summary, - mi.create_tsql, - mi.more_info, - magic_benefit_number, - mi.is_low - FROM #MissingIndexes mi - LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id - AND mi.database_id = sz.database_id - AND mi.schema_name = sz.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) - OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 - ) AS t - WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END - ORDER BY t.is_low, magic_benefit_number DESC - - - END ---------------------------------------- --Abnormal Psychology : Check_id 60-79 ---------------------------------------- - BEGIN RAISERROR(N'check_id 60: XML indexes', 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 60 AS check_id, + SELECT 60 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'XML Indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'XML Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -24172,21 +25786,22 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_XML = 1 OPTION ( RECOMPILE ); + WHERE i.is_XML = 1 + OPTION ( RECOMPILE ); RAISERROR(N'check_id 61: Columnstore indexes', 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 61 AS check_id, + SELECT 61 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, + N'Abnormal Design Pattern' AS findings_group, CASE WHEN i.is_NC_columnstore=1 THEN N'NC Columnstore Index' ELSE N'Clustered Columnstore Index' END AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -24201,13 +25816,13 @@ BEGIN; RAISERROR(N'check_id 62: Spatial indexes', 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 62 AS check_id, + SELECT 62 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Spatial indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Spatial Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -24215,18 +25830,19 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_spatial = 1 OPTION ( RECOMPILE ); + WHERE i.is_spatial = 1 + OPTION ( RECOMPILE ); RAISERROR(N'check_id 63: Compressed indexes', 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 63 AS check_id, + SELECT 63 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Compressed indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Compressed Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, i.index_definition, i.secret_columns, @@ -24234,18 +25850,19 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' OPTION ( RECOMPILE ); + WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' + OPTION ( RECOMPILE ); RAISERROR(N'check_id 64: Partitioned', 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 64 AS check_id, + SELECT 64 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Partitioned indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Partitioned Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -24253,207 +25870,105 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NOT NULL OPTION ( RECOMPILE ); + WHERE i.partition_key_column_name IS NOT NULL + OPTION ( RECOMPILE ); RAISERROR(N'check_id 65: Non-Aligned Partitioned', 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 65 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Non-Aligned index on a partitioned table' AS finding, - i.[database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanity AS iParent ON - i.[object_id]=iParent.[object_id] - AND i.database_id = iParent.database_id - AND i.schema_name = iParent.schema_name - AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ - AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NULL - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently created tables/indexes (1 week)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was created on ' + - CONVERT(NVARCHAR(16),i.create_date,121) + - N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently modified tables/indexes (2 days)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was modified on ' + - CONVERT(NVARCHAR(16),i.modify_date,121) + - N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND /*Exclude recently created tables.*/ - i.create_date < DATEADD(dd,-7,GETDATE()) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; - -- Allowed Ranges: - --int -2,147,483,648 to 2,147,483,647 - --smallint -32,768 to 32,768 - --tinyint 0 to 255 - - 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 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column within ' + - CAST (calc1.percent_remaining AS NVARCHAR(256)) - + N' percent end of range' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - CROSS APPLY ( - SELECT CAST(CASE WHEN ic.increment_value >= 0 - THEN - CASE ic.system_type_name - WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE 999 - END - ELSE --ic.increment_value is negative - CASE ic.system_type_name - WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE -1 - END - END AS NUMERIC(5,1)) AS percent_remaining - ) AS calc1 - WHERE i.index_id IN (1,0) - AND calc1.percent_remaining <= 30 - UNION ALL - SELECT 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column using a negative seed or increment other than 1' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE i.index_id IN (1,0) - AND (ic.seed_value < 0 OR ic.increment_value <> 1) - ORDER BY finding, details DESC OPTION ( RECOMPILE ); + SELECT 65 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Non-Aligned Index on a Partitioned Table' AS finding, + i.[database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanity AS iParent ON + i.[object_id]=iParent.[object_id] + AND i.database_id = iParent.database_id + AND i.schema_name = iParent.schema_name + AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ + AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.partition_key_column_name IS NULL + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Created Tables/Indexes (1 week)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid + N' was created on ' + + CONVERT(NVARCHAR(16),i.create_date,121) + + N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Modified Tables/Indexes (2 days)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid + N' was modified on ' + + CONVERT(NVARCHAR(16),i.modify_date,121) + + N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) + AND /*Exclude recently created tables.*/ + i.create_date < DATEADD(dd,-7,GETDATE()) + OPTION ( RECOMPILE ); RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], database_id, schema_name, - COUNT(*) AS column_count + COUNT(*) AS column_count FROM #IndexColumns ic WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND collation_name <> @collation + AND collation_name <> @collation GROUP BY [object_id], database_id, schema_name ) 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 69 AS check_id, + SELECT 69 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Column collation does not match database collation' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Column Collation Does Not Match Database Collation' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_name + N' has ' + CAST(column_count AS NVARCHAR(20)) + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END @@ -24469,16 +25984,15 @@ BEGIN; AND cc.database_id = i.database_id AND cc.schema_name = i.schema_name WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count, - SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count + database_id, + schema_name, + COUNT(*) AS column_count, + SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count FROM #IndexColumns ic WHERE index_id IN (1,0) /*Heap or clustered only*/ GROUP BY object_id, @@ -24487,13 +26001,13 @@ BEGIN; ) 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 70 AS check_id, + SELECT 70 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Replicated columns' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Replicated Columns' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_name + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) + N' out of ' + CAST(column_count AS NVARCHAR(20)) @@ -24511,28 +26025,27 @@ BEGIN; AND i.schema_name = cc.schema_name WHERE i.index_id IN (1,0) AND replicated_column_count > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 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, more_info ) - SELECT 71 AS check_id, + SELECT 71 AS check_id, NULL AS index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, + N'Abnormal Design Pattern' AS findings_group, N'Cascading Updates or Deletes' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - N'Foreign Key ' + foreign_key_name + + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' + N' has settings:' + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END AS details, - [fk].[database_name] - AS index_definition, + [fk].[database_name] AS index_definition, N'N/A' AS secret_columns, N'N/A' AS index_usage_summary, N'N/A' AS index_size_summary, @@ -24541,21 +26054,42 @@ BEGIN; FROM #ForeignKeys fk WHERE ([delete_referential_action_desc] <> N'NO_ACTION' OR [update_referential_action_desc] <> N'NO_ACTION') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; - IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) - AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) - BEGIN - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + RAISERROR(N'check_id 72: Unindexed foreign keys.', 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, more_info ) + SELECT 72 AS check_id, + NULL AS index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Unindexed Foreign Keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + + N' on ' + QUOTENAME(parent_object_name) + N'' + + N' referencing ' + QUOTENAME(referenced_object_name) + N'' + + N' does not appear to have a supporting index.' AS details, + N'N/A' AS index_definition, + N'N/A' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary, + (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) + AS more_info + FROM #UnindexedForeignKeys AS fk + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 73: In-Memory OLTP', 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 72 AS check_id, + SELECT 73 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - 'Columnstore Indexes are being used in conjunction with trace flag 834. Visit the link to see why this can be a bad idea' AS finding, - [database_name] AS [Database Name], - N'https://support.microsoft.com/en-us/kb/3210239' AS URL, + N'Abnormal Design Pattern' AS findings_group, + N'In-Memory OLTP' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -24563,16 +26097,54 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type IN (5,6) - OPTION ( RECOMPILE ) - END - - END + WHERE i.is_in_memory_oltp = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 74: Identity column with unusual seed', 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 74 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Using a Negative Seed or Increment Other Than 1' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE i.index_id IN (1,0) + AND (ic.seed_value < 0 OR ic.increment_value <> 1) + ORDER BY finding, details DESC + OPTION ( RECOMPILE ); - ---------------------------------------- + ---------------------------------------- --Workaholics: Check_id 80-89 ---------------------------------------- - BEGIN RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -24586,10 +26158,10 @@ BEGIN; 80 AS check_id, i.index_sanity_id AS index_sanity_id, 200 AS Priority, - N'Workaholics' AS findings_group, - N'Scan-a-lots (index_usage_stats)' AS finding, + N'High Workloads' AS findings_group, + N'Scan-a-lots (index-usage-stats)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, + N'https://www.brentozar.com/go/Workaholics' AS URL, REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') + N' scans against ' + i.db_schema_object_indexid + N'. Latest scan: ' + ISNULL(CAST(i.last_user_scan AS NVARCHAR(128)),'?') + N'. ' @@ -24601,8 +26173,8 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id WHERE ISNULL(i.user_scans,0) > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.user_scans * iss.total_reserved_MB DESC; + ORDER BY i.user_scans * iss.total_reserved_MB DESC + OPTION ( RECOMPILE ); RAISERROR(N'check_id 81: Top recent accesses (op stats)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -24614,10 +26186,10 @@ BEGIN; 81 AS check_id, i.index_sanity_id AS index_sanity_id, 200 AS Priority, - N'Workaholics' AS findings_group, - N'Top recent accesses (index_op_stats)' AS finding, + N'High Workloads' AS findings_group, + N'Top Recent Accesses (index-op-stats)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, + N'https://www.brentozar.com/go/Workaholics' AS URL, ISNULL(REPLACE( CONVERT(NVARCHAR(50),CAST((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), N'.00',N'') @@ -24632,87 +26204,20 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id WHERE (ISNULL(iss.total_range_scan_count,0) > 0 OR ISNULL(iss.total_singleton_lookup_count,0) > 0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC; - - - END - - ---------------------------------------- - --Statistics Info: Check_id 90-99 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 90: Outdated 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 ) - SELECT 90 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Statistic Abandonment Issues', - s.database_name, - '' AS URL, - 'Statistics on this table were last updated ' + - CASE s.last_statistics_update WHEN NULL THEN N' NEVER ' - ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + - ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + - ' modifications in that time, which is ' + - CONVERT(NVARCHAR(100), s.percent_modifications) + - '% of the table.' - END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 7) - AND s.percent_modifications >= 10. - AND s.rows >= 10000 + ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Antisocial Samples', - s.database_name, - '' AS URL, - 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows_sampled < 1. - AND s.rows >= 10000 - 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 ) - SELECT 92 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Cyberphobic Samples', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.no_recompute = 1 RAISERROR(N'check_id 93: Statistics with filters', 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 93 AS check_id, 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Filter Fixation', + 'Statistics Warnings' AS findings_group, + 'Statistics With Filters', s.database_name, - '' AS URL, + 'https://www.brentozar.com/go/stats' AS URL, 'The statistic ' + QUOTENAME(s.statistics_name) + ' is filtered on [' + s.filter_definition + ']. It could be part of a filtered index, or just a filtered statistic. This is purely informational.' 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, @@ -24720,39 +26225,16 @@ BEGIN; 'N/A' AS index_size_summary FROM #Statistics AS s WHERE s.has_filter = 1 + OPTION ( RECOMPILE ); - END - - ---------------------------------------- - --Computed Column Info: Check_id 99-109 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 99: Computed Columns 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 ) - SELECT 99 AS check_id, - 50 AS Priority, - 'Cold Calculators' AS findings_group, - 'Serial Forcer' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition - + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_function = 1 RAISERROR(N'check_id 100: Computed Columns that are not Persisted.', 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 100 AS check_id, 200 AS Priority, - 'Cold Calculators' AS findings_group, - 'Definition Defeatists' AS finding, + 'Repeated Calculations' AS findings_group, + 'Computed Columns Not Persisted' AS finding, cc.database_name, '' AS URL, 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is not persisted, which means it will be calculated when a query runs.' + @@ -24764,18 +26246,16 @@ BEGIN; 'N/A' AS index_size_summary FROM #ComputedColumns AS cc WHERE cc.is_persisted = 0 + OPTION ( RECOMPILE ); - ---------------------------------------- - --Temporal Table Info: Check_id 110-119 - ---------------------------------------- RAISERROR(N'check_id 110: Temporal Tables.', 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 110 AS check_id, 200 AS Priority, - 'Temporal Tables' AS findings_group, - 'Obsessive Compulsive Tables', + 'Abnormal Design Pattern' AS findings_group, + 'Temporal Tables', t.database_name, '' AS URL, 'The table ' + QUOTENAME(t.schema_name) + '.' + QUOTENAME(t.table_name) + ' is a temporal table, with rows versioned in ' @@ -24786,11 +26266,102 @@ BEGIN; 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #TemporalTables AS t + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 121: Optimized For Sequential Keys.', 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 121 AS check_id, + 200 AS Priority, + 'Specialized Indexes' AS findings_group, + 'Optimized For Sequential Keys', + i.database_name, + '' AS URL, + 'The table ' + QUOTENAME(i.schema_name) + '.' + QUOTENAME(i.object_name) + ' is optimized for sequential keys.' AS details, + '' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #IndexSanity AS i + WHERE i.optimize_for_sequential_key = 1 + 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 ); - END - + 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 */ + + + + + + + + RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; IF DATEDIFF(MM, @VersionDate, GETDATE()) > 6 BEGIN @@ -24799,9 +26370,9 @@ BEGIN; VALUES ( -1, 0 , 'Outdated sp_BlitzIndex', 'sp_BlitzIndex is Over 6 Months Old', 'http://FirstResponderKit.org/', 'Fine wine gets better with age, but this ' + @ScriptVersionName + ' is more like bad cheese. Time to get a new one.', - N'',N'',N'' + @DaysUptimeInsertValue,N'',N'' ); - END + END; IF EXISTS(SELECT * FROM #BlitzIndexResults) BEGIN @@ -24811,10 +26382,9 @@ BEGIN; @ScriptVersionName, CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' + @DaysUptimeInsertValue,N'',N'' ); - END + END; ELSE IF @Mode = 0 OR (@GetAllDatabases = 1 AND @Mode <> 4) BEGIN INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, @@ -24823,257 +26393,462 @@ BEGIN; @ScriptVersionName, CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' + @DaysUptimeInsertValue, N'',N'' + ); + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, 0 , + N'No Major Problems Found', + N'Nice Work!', + N'http://FirstResponderKit.org', + N'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', + N'The default Mode 0 only looks for very serious index issues.', + @DaysUptimeInsertValue, N'' + ); + + END; + ELSE + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, N'',N'' ); INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, index_usage_summary, index_size_summary ) VALUES ( 1, 0 , - 'No Major Problems Found', - 'Nice Work!', - 'http://FirstResponderKit.org', 'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', 'The new default Mode 0 only looks for very serious index issues.', '', '' + N'No Problems Found', + N'Nice job! Or more likely, you have a nearly empty database.', + N'http://FirstResponderKit.org', 'Time to go read some blog posts.', + @DaysUptimeInsertValue, N'', N'' ); - END + END; + + RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; + + /*Return results.*/ + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [priority] INT, + [finding] NVARCHAR(4000), + [database_name] NVARCHAR(128), + [details] NVARCHAR(MAX), + [index_definition] NVARCHAR(MAX), + [secret_columns] NVARCHAR(MAX), + [index_usage_summary] NVARCHAR(MAX), + [index_size_summary] NVARCHAR(MAX), + [more_info] NVARCHAR(MAX), + [url] NVARCHAR(MAX), + [create_tsql] NVARCHAR(MAX), + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [priority], + [finding], + [database_name], + [details], + [index_definition], + [secret_columns], + [index_usage_summary], + [index_size_summary], + [more_info], + [url], + [create_tsql], + [sample_query_plan] + ) + SELECT + ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + Priority, ISNULL(br.findings_group,N'''') + + CASE WHEN ISNULL(br.finding,N'''') <> N'''' THEN N'': '' ELSE N'''' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'''') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'''') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'''') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END ELSE - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'No Problems Found', - 'Nice job! Or more likely, you have a nearly empty database.', - 'http://FirstResponderKit.org', 'Time to go read some blog posts.', '', '', '' - ); + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + SELECT Priority, ISNULL(br.findings_group,N'') + + CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE); + END; + + END; + + END /* End @Mode=0 or 4 (diagnose)*/ + - END - RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; - - /*Return results.*/ - IF (@Mode = 0) - BEGIN - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - WHERE br.check_id IN (0, 1, 11, 22, 43, 68, 50, 60, 61, 62, 63, 64, 65, 72) - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - END - ELSE IF (@Mode = 4) - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - END; /* End @Mode=0 or 4 (diagnose)*/ - ELSE IF @Mode=1 /*Summarize*/ + + + ELSE IF (@Mode=1) /*Summarize*/ BEGIN --This mode is to give some overall stats on the database. - RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; - - SELECT DB_NAME(i.database_id) AS [Database Name], - CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], - CAST(CAST(SUM(sz.total_reserved_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], - CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], - CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], - CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], - CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], - SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN - CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) - ELSE 0 END AS [ratio table: NC Indexes], - SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], - CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Heaps GB], - SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], - SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], - CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], - SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], - SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], - MAX(total_rows) AS [Max Row Count], - CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max Table GB], - CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], - MIN(create_date) AS [Oldest Create Date], - MAX(create_date) AS [Most Recent Create Date], - MAX(modify_date) AS [Most Recent Modify Date], - 1 AS [Display Order] - FROM #IndexSanity AS i - --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz - ON i.index_sanity_id=sz.index_sanity_id - GROUP BY DB_NAME(i.database_id) - UNION ALL - SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,0 AS display_order - ORDER BY [Display Order] ASC - OPTION (RECOMPILE); - - END /* End @Mode=1 (summarize)*/ - ELSE IF @Mode=2 /*Index Detail*/ - BEGIN - --This mode just spits out all the detail without filters. - --This supports slicing AND dicing in Excel - RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; - - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT - DECLARE @ValidOutputLocation BIT - DECLARE @LinkedServerDBCheck NVARCHAR(2000) - DECLARE @ValidLinkedServerDB INT - DECLARE @tmpdbchk table (cnt int) - DECLARE @StringToExecute NVARCHAR(MAX); - - IF @OutputServerName IS NOT NULL + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) BEGIN - IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + + IF NOT @SchemaExists = 1 BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; END - ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + + IF @TableExists = 0 BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')' - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk) - IF (@ValidLinkedServerDB > 0) + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [object_count] INT, + [reserved_gb] NUMERIC(29,1), + [reserved_lob_gb] NUMERIC(29,1), + [reserved_row_overflow_gb] NUMERIC(29,1), + [clustered_table_count] INT, + [clustered_table_gb] NUMERIC(29,1), + [nc_index_count] INT, + [nc_index_gb] NUMERIC(29,1), + [table_nc_index_ratio] NUMERIC(29,1), + [heap_count] INT, + [heap_gb] NUMERIC(29,1), + [partitioned_table_count] INT, + [partitioned_nc_count] INT, + [partitioned_gb] NUMERIC(29,1), + [filtered_index_count] INT, + [indexed_view_count] INT, + [max_table_row_count] INT, + [max_table_gb] NUMERIC(29,1), + [max_nc_index_gb] NUMERIC(29,1), + [table_count_over_1gb] INT, + [table_count_over_10gb] INT, + [table_count_over_100gb] INT, + [nc_index_count_over_1gb] INT, + [nc_index_count_over_10gb] INT, + [nc_index_count_over_100gb] INT, + [min_create_date] DATETIME, + [max_create_date] DATETIME, + [max_modify_date] DATETIME, + [display_order] INT, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 BEGIN - SET @ValidOutputServer = 1 - SET @ValidOutputLocation = 1 - END + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; ELSE - RAISERROR('The specified database was not found on the output server', 16, 0) - END - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0) - END - END + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [object_count], + [reserved_gb], + [reserved_lob_gb], + [reserved_row_overflow_gb], + [clustered_table_count], + [clustered_table_gb], + [nc_index_count], + [nc_index_gb], + [table_nc_index_ratio], + [heap_count], + [heap_gb], + [partitioned_table_count], + [partitioned_nc_count], + [partitioned_gb], + [filtered_index_count], + [indexed_view_count], + [max_table_row_count], + [max_table_gb], + [max_nc_index_gb], + [table_count_over_1gb], + [table_count_over_10gb], + [table_count_over_100gb], + [nc_index_count_over_1gb], + [nc_index_count_over_10gb], + [nc_index_count_over_100gb], + [min_create_date], + [max_create_date], + [max_modify_date], + [display_order] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! initial columns are not casted to nvarchar due to not outputing informational line + DB_NAME(i.database_id) AS [Database Name], + COUNT(*) AS [Number Objects], + CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS [All GB], + CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS [LOB GB], + CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS [Row Overflow GB], + SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '''' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don''t lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + ORDER BY [Display Order] ASC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END; /* @ValidOutputLocation = 1 */ ELSE BEGIN - IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - EXEC(@StringToExecute); - - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - SET @OutputDatabaseName = '[tempdb]'; - SET @OutputSchemaName = '[dbo]'; - SET @ValidOutputLocation = 1; - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1 - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0) - END - ELSE - BEGIN - SET @ValidOutputLocation = 0 - END - END + IF(@OutputType <> 'NONE') + BEGIN + + RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; + + SELECT DB_NAME(i.database_id) AS [Database Name], + CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], + CAST(CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], + CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], + CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], + CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + UNION ALL + SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,0 AS display_order + ORDER BY [Display Order] ASC + OPTION (RECOMPILE); + END; + END; + + END; /* End @Mode=1 (summarize)*/ + + + + + + + + + ELSE IF (@Mode=2) /*Index Detail*/ + BEGIN + --This mode just spits out all the detail without filters. + --This supports slicing AND dicing in Excel + RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; - /* @OutputTableName lets us export the results to a permanent table */ - DECLARE @RunID UNIQUEIDENTIFIER; - SET @RunID = NEWID(); - IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) BEGIN - DECLARE @TableExists BIT; - DECLARE @SchemaExists BIT; - SET @StringToExecute = - N'SET @SchemaExists = 0; - SET @TableExists = 0; - IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - SET @SchemaExists = 1 - IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 1'; - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT - IF @SchemaExists = 1 BEGIN IF @TableExists = 0 @@ -25088,11 +26863,13 @@ BEGIN; [database_name] NVARCHAR(128), [schema_name] NVARCHAR(128), [table_name] NVARCHAR(128), - [index_name] NVARCHAR(128), + [index_name] NVARCHAR(128), + [Drop_Tsql] NVARCHAR(MAX), + [Create_Tsql] NVARCHAR(MAX), [index_id] INT, [db_schema_object_indexid] NVARCHAR(500), [object_type] NVARCHAR(15), - [index_definition] NVARCHAR(4000), + [index_definition] NVARCHAR(MAX), [key_column_names_with_sort_order] NVARCHAR(MAX), [count_key_columns] INT, [include_column_names] NVARCHAR(MAX), @@ -25102,11 +26879,13 @@ BEGIN; [partition_key_column_name] NVARCHAR(MAX), [filter_definition] NVARCHAR(MAX), [is_indexed_view] BIT, - [is_primary_key] BIT, + [is_primary_key] BIT, + [is_unique_constraint] BIT, [is_XML] BIT, [is_spatial] BIT, [is_NC_columnstore] BIT, [is_CX_columnstore] BIT, + [is_in_memory_oltp] BIT, [is_disabled] BIT, [is_hypothetical] BIT, [is_padded] BIT, @@ -25120,6 +26899,11 @@ BEGIN; [user_updates] BIGINT, [reads_per_write] MONEY, [index_usage_summary] NVARCHAR(200), + [total_singleton_lookup_count] BIGINT, + [total_range_scan_count] BIGINT, + [total_leaf_delete_count] BIGINT, + [total_leaf_update_count] BIGINT, + [index_op_stats] NVARCHAR(200), [partition_count] INT, [total_rows] BIGINT, [total_reserved_MB] NUMERIC(29,2), @@ -25136,44 +26920,39 @@ BEGIN; [avg_page_lock_wait_in_ms] BIGINT, [total_index_lock_promotion_attempt_count] BIGINT, [total_index_lock_promotion_count] BIGINT, - [data_compression_desc] VARCHAR(8000), + [total_forwarded_fetch_count] BIGINT, + [data_compression_desc] NVARCHAR(4000), + [page_latch_wait_count] BIGINT, + [page_latch_wait_in_ms] BIGINT, + [page_io_latch_wait_count] BIGINT, + [page_io_latch_wait_in_ms] BIGINT, [create_date] DATETIME, [modify_date] DATETIME, [more_info] NVARCHAR(500), [display_order] INT, CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) - );' + );'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); IF @ValidOutputServer = 1 BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,'''','''''') + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END + END; ELSE BEGIN EXEC(@StringToExecute); - END - END /* @TableExists = 0 */ + END; + END; /* @TableExists = 0 */ - SET @StringToExecute = - N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 0 - ELSE - SET @TableExists = 1'; - - SET @TableExists = NULL - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT', @TableExists OUTPUT + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; IF @TableExists = 1 BEGIN @@ -25186,7 +26965,9 @@ BEGIN; [database_name], [schema_name], [table_name], - [index_name], + [index_name], + [Drop_Tsql], + [Create_Tsql], [index_id], [db_schema_object_indexid], [object_type], @@ -25200,11 +26981,13 @@ BEGIN; [partition_key_column_name], [filter_definition], [is_indexed_view], - [is_primary_key], + [is_primary_key], + [is_unique_constraint], [is_XML], [is_spatial], [is_NC_columnstore], [is_CX_columnstore], + [is_in_memory_oltp], [is_disabled], [is_hypothetical], [is_padded], @@ -25218,6 +27001,11 @@ BEGIN; [user_updates], [reads_per_write], [index_usage_summary], + [total_singleton_lookup_count], + [total_range_scan_count], + [total_leaf_delete_count], + [total_leaf_update_count], + [index_op_stats], [partition_count], [total_rows], [total_reserved_MB], @@ -25234,7 +27022,12 @@ BEGIN; [avg_page_lock_wait_in_ms], [total_index_lock_promotion_attempt_count], [total_index_lock_promotion_count], + [total_forwarded_fetch_count], [data_compression_desc], + [page_latch_wait_count], + [page_latch_wait_in_ms], + [page_io_latch_wait_count], + [page_io_latch_wait_in_ms], [create_date], [modify_date], [more_info], @@ -25248,13 +27041,28 @@ BEGIN; i.[database_name] AS [Database Name], i.[schema_name] AS [Schema Name], i.[object_name] AS [Object Name], - ISNULL(i.index_name, '''') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], + ISNULL(i.index_name, '''') AS [Index Name], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.index_definition <> ''[HEAP]'' + THEN N''--DROP INDEX ''+ QUOTENAME(i.index_name) + N'' ON '' + QUOTENAME(i.[database_name]) + N''.'' + + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + N'';'' + ELSE N'''' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = ''[HEAP]'' THEN N'''' + ELSE N''--'' + ict.create_tsql END AS [Create TSQL], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], db_schema_object_indexid AS [Details: schema.table.index(indexid)], CASE WHEN index_id IN ( 1, 0 ) THEN ''TABLE'' ELSE ''NonClustered'' END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + LEFT(index_definition,4000) AS [Definition: [Property]] ColumnName {datatype maxbytes}], ISNULL(LTRIM(key_column_names_with_sort_order), '''') AS [Key Column Names With Sort], ISNULL(count_key_columns, 0) AS [Count Key Columns], ISNULL(include_column_names, '''') AS [Include Column Names], @@ -25265,10 +27073,12 @@ BEGIN; ISNULL(filter_definition, '''') AS [Filter Definition], is_indexed_view AS [Is Indexed View], is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint], is_XML AS [Is XML], is_spatial AS [Is Spatial], is_NC_columnstore AS [Is NC Columnstore], is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], is_disabled AS [Is Disabled], is_hypothetical AS [Is Hypothetical], is_padded AS [Is Padded], @@ -25282,6 +27092,11 @@ BEGIN; user_updates AS [User Updates], reads_per_write AS [Reads Per Write], index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], sz.partition_count AS [Partition Count], sz.total_rows AS [Rows], sz.total_reserved_MB AS [Reserved MB], @@ -25298,158 +27113,434 @@ BEGIN; sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], sz.data_compression_desc AS [Data Compression], + sz.page_latch_wait_count, + sz.page_latch_wait_in_ms, + sz.page_io_latch_wait_count, + sz.page_io_latch_wait_in_ms, i.create_date AS [Create Date], i.modify_date AS [Modify Date], more_info AS [More Info], 1 AS [Display Order] FROM #IndexSanity AS i LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] OPTION (RECOMPILE);'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); EXEC(@StringToExecute); - END /* @TableExists = 1 */ + END; /* @TableExists = 1 */ ELSE - RAISERROR('Creation of the output table failed.', 16, 0) - END /* @TableExists = 0 */ + RAISERROR('Creation of the output table failed.', 16, 0); + END; /* @TableExists = 0 */ ELSE - RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0) - END /* @ValidOutputLocation = 1 */ + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + END; /* @ValidOutputLocation = 1 */ ELSE + IF(@OutputType <> 'NONE') + BEGIN + SELECT i.[database_name] AS [Database Name], + i.[schema_name] AS [Schema Name], + i.[object_name] AS [Object Name], + ISNULL(i.index_name, '') AS [Index Name], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], + db_schema_object_indexid AS [Details: schema.table.index(indexid)], + CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' + ELSE 'NonClustered' + END AS [Object Type], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], + ISNULL(count_key_columns, 0) AS [Count Key Columns], + ISNULL(include_column_names, '') AS [Include Column Names], + ISNULL(count_included_columns,0) AS [Count Included Columns], + ISNULL(secret_columns,'') AS [Secret Column Names], + ISNULL(count_secret_columns,0) AS [Count Secret Columns], + ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], + ISNULL(filter_definition, '') AS [Filter Definition], + is_indexed_view AS [Is Indexed View], + is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint] , + is_XML AS [Is XML], + is_spatial AS [Is Spatial], + is_NC_columnstore AS [Is NC Columnstore], + is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], + is_disabled AS [Is Disabled], + is_hypothetical AS [Is Hypothetical], + is_padded AS [Is Padded], + fill_factor AS [Fill Factor], + is_referenced_by_foreign_key AS [Is Reference by Foreign Key], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Update], + total_reads AS [Total Reads], + user_updates AS [User Updates], + reads_per_write AS [Reads Per Write], + index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], + sz.partition_count AS [Partition Count], + sz.total_rows AS [Rows], + sz.total_reserved_MB AS [Reserved MB], + sz.total_reserved_LOB_MB AS [Reserved LOB MB], + sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], + sz.index_size_summary AS [Index Size], + sz.total_row_lock_count AS [Row Lock Count], + sz.total_row_lock_wait_count AS [Row Lock Wait Count], + sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], + sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], + sz.total_page_lock_count AS [Page Lock Count], + sz.total_page_lock_wait_count AS [Page Lock Wait Count], + sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], + sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], + sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], + sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.page_latch_wait_count AS [Page Latch Wait Count], + sz.page_latch_wait_in_ms AS [Page Latch Wait ms], + sz.page_io_latch_wait_count AS [Page IO Latch Wait Count], + sz.page_io_latch_wait_in_ms as [Page IO Latch Wait ms], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], + sz.data_compression_desc AS [Data Compression], + i.create_date AS [Create Date], + i.modify_date AS [Modify Date], + more_info AS [More Info], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(i.index_name) + N' ON ' + QUOTENAME(i.[database_name]) + N'.' + + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + N';' + ELSE N'' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = '[HEAP]' THEN N'' + ELSE N'--' + ict.create_tsql END AS [Create TSQL], + 1 AS [Display Order] + INTO #Mode2Temp + FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id + OPTION(RECOMPILE); + + IF @@ROWCOUNT > 0 + BEGIN + SELECT + sz.* + FROM #Mode2Temp AS sz + ORDER BY /* Shout out to DHutmacher */ + /*DESC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'desc' THEN sz.[Rows] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'desc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END DESC, + /*ASC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'asc' THEN sz.[Rows] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'asc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END ASC, + sz.[Database Name], [Schema Name], [Object Name], [Index ID] + OPTION (RECOMPILE); + END + ELSE + BEGIN + SELECT + DatabaseDetails = + N'Database ' + + ISNULL(@DatabaseName, DB_NAME()) + + N' has ' + + ISNULL(RTRIM(@Rowcount), 0) + + N' partitions.', + BringThePain = + CASE + WHEN @BringThePain IN (0, 1) AND ISNULL(@Rowcount, 0) = 0 + THEN N'Check the database name, it looks like nothing is here.' + WHEN @BringThePain = 0 AND ISNULL(@Rowcount, 0) > 0 + THEN N'Please re-run with @BringThePain = 1' + END; + END + END; + END; /* End @Mode=2 (index detail)*/ + + + - SELECT i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' - ELSE 'NonClustered' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], - ISNULL(filter_definition, '') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE); - - - - END /* End @Mode=2 (index detail)*/ - ELSE IF @Mode=3 /*Missing index Detail*/ + + + + + ELSE IF (@Mode=3) /*Missing index Detail*/ BEGIN + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT - mi.database_name AS [Database Name], - mi.[schema_name] AS [Schema], - mi.table_name AS [Table], - CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) - AS [Magic Benefit Number], - mi.missing_index_details AS [Missing Index Details], - mi.avg_total_user_cost AS [Avg Query Cost], - mi.avg_user_impact AS [Est Index Improvement], - mi.user_seeks AS [Seeks], - mi.user_scans AS [Scans], - mi.unique_compiles AS [Compiles], - mi.equality_columns AS [Equality Columns], - mi.inequality_columns AS [Inequality Columns], - mi.included_columns AS [Included Columns], - mi.index_estimated_impact AS [Estimated Impact], - mi.create_tsql AS [Create TSQL], - mi.more_info AS [More Info], - 1 AS [Display Order], - mi.is_low - FROM #MissingIndexes AS mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - UNION ALL - SELECT - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - 100000000000, - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL, 0 AS [Display Order], NULL AS is_low - ORDER BY [Display Order] ASC, is_low, [Magic Benefit Number] DESC - OPTION (RECOMPILE); + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END - END /* End @Mode=3 (index detail)*/ -END + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [schema_name] NVARCHAR(128), + [table_name] NVARCHAR(128), + [magic_benefit_number] BIGINT, + [missing_index_details] NVARCHAR(MAX), + [avg_total_user_cost] NUMERIC(29,4), + [avg_user_impact] NUMERIC(29,1), + [user_seeks] BIGINT, + [user_scans] BIGINT, + [unique_compiles] BIGINT, + [equality_columns_with_data_type] NVARCHAR(MAX), + [inequality_columns_with_data_type] NVARCHAR(MAX), + [included_columns_with_data_type] NVARCHAR(MAX), + [index_estimated_impact] NVARCHAR(256), + [create_tsql] NVARCHAR(MAX), + [more_info] NVARCHAR(600), + [display_order] INT, + [is_low] BIT, + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + SET @StringToExecute = + N'WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [schema_name], + [table_name], + [magic_benefit_number], + [missing_index_details], + [avg_total_user_cost], + [avg_user_impact], + [user_seeks], + [user_scans], + [unique_compiles], + [equality_columns_with_data_type], + [inequality_columns_with_data_type], + [included_columns_with_data_type], + [index_estimated_impact], + [create_tsql], + [more_info], + [display_order], + [is_low], + [sample_query_plan] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! CTE block is above insert in the copied SQL + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC sp_executesql @StringToExecute, N'@DaysUptime NUMERIC(23,2), @ShowAllMissingIndexRequests BIT', @DaysUptime = @DaysUptime, @ShowAllMissingIndexRequests = @ShowAllMissingIndexRequests; + + END; /* @ValidOutputLocation = 1 */ + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + SELECT + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + UNION ALL + SELECT + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + 100000000000, + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL, NULL, NULL, NULL, 0 AS [Display Order], NULL AS is_low, NULL + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE); + END; + + + IF (@BringThePain = 1 + AND @DatabaseName IS NOT NULL + AND @GetAllDatabases = 0) + + BEGIN + EXEC sp_BlitzCache @SortOrder = 'sp_BlitzIndex', @DatabaseName = @DatabaseName, @BringThePain = 1, @QueryFilter = 'statement', @HideSummary = 1; + END; + + END; + + + + + + END; /* End @Mode=3 (index detail)*/ + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'finishing at %s',0,1, @d) WITH NOWAIT; +END /* End @TableName IS NULL (mode 0/1/2/3/4) */ END TRY BEGIN CATCH RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); RAISERROR (@msg, @ErrorSeverity, @@ -25463,7297 +27554,13636 @@ BEGIN CATCH END CATCH; GO IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); +BEGIN + EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); +END; GO -ALTER PROCEDURE dbo.sp_BlitzLock +ALTER PROCEDURE + dbo.sp_BlitzLock ( - @Top INT = 2147483647, - @DatabaseName NVARCHAR(256) = NULL, - @StartDate DATETIME = '19000101', - @EndDate DATETIME = '99991231', - @ObjectName NVARCHAR(1000) = NULL, - @StoredProcName NVARCHAR(1000) = NULL, - @AppName NVARCHAR(256) = NULL, - @HostName NVARCHAR(256) = NULL, - @LoginName NVARCHAR(256) = NULL, - @EventSessionPath VARCHAR(256) = 'system_health*.xel', - @Debug BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @DatabaseName sysname = NULL, + @StartDate datetime = NULL, + @EndDate datetime = NULL, + @ObjectName nvarchar(1024) = NULL, + @StoredProcName nvarchar(1024) = NULL, + @AppName sysname = NULL, + @HostName sysname = NULL, + @LoginName sysname = NULL, + @EventSessionName sysname = N'system_health', + @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, + @VersionDate datetime = NULL OUTPUT, + @VersionCheckMode bit = 0, + @OutputDatabaseName sysname = NULL, + @OutputSchemaName sysname = N'dbo', /*ditto as below*/ + @OutputTableName sysname = N'BlitzLock', /*put a standard here no need to check later in the script*/ + @ExportToExcel bit = 0 ) +WITH RECOMPILE AS BEGIN + SET STATISTICS XML OFF; + SET NOCOUNT ON; + SET XACT_ABORT OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @Version = '8.26', @VersionDate = '20251002'; -DECLARE @Version VARCHAR(30); -SET @Version = '1.0'; -SET @VersionDate = '20171201'; + IF @VersionCheckMode = 1 + BEGIN + RETURN; + END; + IF @Help = 1 + BEGIN + PRINT N' + /* + sp_BlitzLock from http://FirstResponderKit.org - IF @Help = 1 PRINT ' - /* - sp_BlitzLock from http://FirstResponderKit.org - - This script checks for and analyzes deadlocks from the system health session or a custom extended event path + This script checks for and analyzes deadlocks from the system health session or a custom extended event path - Variables you can use: - @Top: Use if you want to limit the number of deadlocks to return. - This is ordered by event date ascending + Variables you can use: - @DatabaseName: If you want to filter to a specific database + /*Filtering parameters*/ + @DatabaseName: If you want to filter to a specific database - @StartDate: The date you want to start searching on. + @StartDate: The date you want to start searching on, defaults to last 7 days - @EndDate: The date you want to stop searching on. + @EndDate: The date you want to stop searching on, defaults to current date - @ObjectName: If you want to filter to a specific able. - The object name has to be fully qualified ''Database.Schema.Table'' + @ObjectName: If you want to filter to a specific able. + The object name has to be fully qualified ''Database.Schema.Table'' - @StoredProcName: If you want to search for a single stored proc - The proc name has to be fully qualified ''Database.Schema.Sproc'' - - @AppName: If you want to filter to a specific application - - @HostName: If you want to filter to a specific host - - @LoginName: If you want to filter to a specific login + @StoredProcName: If you want to search for a single stored proc + The proc name has to be fully qualified ''Database.Schema.Sproc'' - @EventSessionPath: If you want to point this at an XE session rather than the system health session. - - - - 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. + @AppName: If you want to filter to a specific application + + @HostName: If you want to filter to a specific host + + @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'', ''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 - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) + @OutputTableName: Specify table name to to output information to a specific table - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + /*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. + Known limitations of this version: + - Only SQL Server 2012 and newer is supported + - If your tables have weird characters in them (https://en.wikipedia.org/wiki/List_of_xml_and_HTML_character_entity_references) you may get errors trying to parse the xml. + I took a long look at this one, and: + 1) Trying to account for all the weird places these could crop up is a losing effort. + 2) Replace is slow af on lots of xml. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) MIT License - - All other copyright for sp_BlitzLock are held by Brent Ozar Unlimited, 2017. - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited - 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: + 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 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. + 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. - */'; + */'; + + RETURN; + END; /* @Help = 1 */ + + /*Declare local variables used in the procudure*/ + DECLARE + @DatabaseId int = + DB_ID(@DatabaseName), + @ProductVersion nvarchar(128) = + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + @ProductVersionMajor float = + SUBSTRING + ( + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + 1, + CHARINDEX('.', CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128))) + 1 + ), + @ProductVersionMinor int = + PARSENAME + ( + CONVERT + ( + varchar(32), + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) + ), + 2 + ), + @ObjectFullName nvarchar(MAX) = N'', + @Azure bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 5 + THEN 1 + ELSE 0 + END, + @MI bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 8 + THEN 1 + ELSE 0 + END, + @RDS bit = + CASE + WHEN LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND DB_ID('rdsadmin') IS NULL + THEN 0 + ELSE 1 + END, + @d varchar(40) = '', + @StringToExecute nvarchar(4000) = N'', + @StringToExecuteParams nvarchar(500) = N'', + @r sysname = NULL, + @OutputTableFindings nvarchar(100) = N'[BlitzLockFindings]', + @DeadlockCount int = 0, + @ServerName sysname = @@SERVERNAME, + @OutputDatabaseCheck bit = -1, + @SessionId int = 0, + @TargetSessionId int = 0, + @FileName nvarchar(4000) = N'', + @inputbuf_bom nvarchar(1) = CONVERT(nvarchar(1), 0x0a00, 0), + @deadlock_result nvarchar(MAX) = N'', + @StartDateOriginal datetime = @StartDate, + @EndDateOriginal datetime = @EndDate, + @StartDateUTC datetime, + @EndDateUTC datetime, + @extract_sql nvarchar(MAX), + @validation_sql nvarchar(MAX), + @xe bit, + @xd bit; + + /*Temporary objects used in the procedure*/ + DECLARE + @sysAssObjId AS table + ( + database_id int, + partition_id bigint, + schema_name sysname, + table_name sysname + ); + CREATE TABLE + #x + ( + x xml NOT NULL + DEFAULT N'x' + ); - DECLARE @ProductVersion NVARCHAR(128); - DECLARE @ProductVersionMajor FLOAT; - DECLARE @ProductVersionMinor INT; + CREATE TABLE + #deadlock_data + ( + deadlock_xml xml NOT NULL + DEFAULT N'x' + ); - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); + CREATE TABLE + #t + ( + id int NOT NULL + ); - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); + CREATE TABLE + #deadlock_findings + ( + id int IDENTITY PRIMARY KEY, + check_id int NOT NULL, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000), + sort_order bigint + ); + /*Set these to some sane defaults if NULLs are passed in*/ + /*Normally I'd hate this, but we RECOMPILE everything*/ - IF @ProductVersionMajor < 11.0 - BEGIN - RAISERROR( - 'sp_BlitzLock will throw a bunch of angry errors on versions of SQL Server earlier than 2012.', - 0, - 1) WITH NOWAIT; - RETURN; + SELECT + @StartDate = + CASE + WHEN @StartDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + DATEADD + ( + DAY, + -7, + SYSDATETIME() + ) + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @StartDate + ) + END, + @EndDate = + CASE + WHEN @EndDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + SYSDATETIME() + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @EndDate + ) END; - IF @Top IS NULL - SET @Top = 2147483647; + SELECT + @StartDateUTC = @StartDate, + @EndDateUTC = @EndDate; + + IF + ( + @MI = 1 + AND @EventSessionName = N'system_health' + AND @TargetSessionType IS NULL + ) + BEGIN + SET + @TargetSessionType = N'ring_buffer'; + END; - IF @StartDate IS NULL - SET @StartDate = '19000101'; + 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; - IF @EndDate IS NULL - SET @EndDate = '99991231'; - + /* Add this after the existing parameter validations */ + IF @TargetSessionType = N'table' + BEGIN + IF @TargetDatabaseName IS NULL + BEGIN + SET @TargetDatabaseName = DB_NAME(); + END; - IF OBJECT_ID('tempdb..#deadlock_data') IS NOT NULL - DROP TABLE #deadlock_data; + IF @TargetSchemaName IS NULL + BEGIN + SET @TargetSchemaName = N'dbo'; + END; - IF OBJECT_ID('tempdb..#deadlock_process') IS NOT NULL - DROP TABLE #deadlock_process; + 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 OBJECT_ID('tempdb..#deadlock_stack') IS NOT NULL - DROP TABLE #deadlock_stack; - IF OBJECT_ID('tempdb..#deadlock_resource') IS NOT NULL - DROP TABLE #deadlock_resource; + IF @Azure = 0 + AND LOWER(@TargetSessionType) <> N'table' + BEGIN + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.server_event_sessions AS ses + JOIN sys.dm_xe_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; - IF OBJECT_ID('tempdb..#deadlock_owner_waiter') IS NOT NULL - DROP TABLE #deadlock_owner_waiter; + IF @Azure = 1 + AND LOWER(@TargetSessionType) <> N'table' + BEGIN + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.database_event_sessions AS ses + JOIN sys.dm_xe_database_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; - IF OBJECT_ID('tempdb..#deadlock_findings') IS NOT NULL - DROP TABLE #deadlock_findings; + IF @OutputDatabaseName IS NOT NULL + BEGIN /*IF databaseName is set, do some sanity checks and put [] around def.*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@OutputDatabaseName set to %s, checking validity at %s', 0, 1, @OutputDatabaseName, @d) WITH NOWAIT; - CREATE TABLE #deadlock_findings - ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - check_id INT NOT NULL, - database_name NVARCHAR(256), - object_name NVARCHAR(1000), - finding_group NVARCHAR(100), - finding NVARCHAR(4000) - ); + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name = @OutputDatabaseName + ) /*If database is invalid raiserror and set bitcheck*/ + BEGIN + RAISERROR('Database Name (%s) for output of table is invalid please, Output to Table will not be performed', 0, 1, @OutputDatabaseName) WITH NOWAIT; + SET @OutputDatabaseCheck = -1; /* -1 invalid/false, 0 = good/true */ + END; + ELSE + BEGIN + SET @OutputDatabaseCheck = 0; + + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + 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 s.name =''' + + @OutputSchemaName + + N''';', + @StringToExecuteParams = + N'@r sysname OUTPUT'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; + + IF @Debug = 1 + BEGIN + RAISERROR('@r is set to: %s for schema name %s and table name %s', 0, 1, @r, @OutputSchemaName, @OutputTableName) WITH NOWAIT; + END; + /*protection spells*/ + SELECT + @ObjectFullName = + QUOTENAME(@OutputDatabaseName) + + N'.' + + QUOTENAME(@OutputSchemaName) + + N'.' + + QUOTENAME(@OutputTableName), + @OutputDatabaseName = + QUOTENAME(@OutputDatabaseName), + @OutputTableName = + QUOTENAME(@OutputTableName), + @OutputSchemaName = + QUOTENAME(@OutputSchemaName); + + IF (@r IS NOT NULL) /*if it is not null, there is a table, so check for newly added columns*/ + BEGIN + /* If the table doesn't have the new spid column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''spid'') + /*Add spid column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD spid smallint NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new wait_resource column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''wait_resource'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD wait_resource nvarchar(MAX) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_1'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_1 varchar(500) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_2'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_2 varchar(500) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new lock mode column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''lock_mode'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD lock_mode nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new status column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''status'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD status nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + 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 */ + BEGIN + SELECT + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableName + + N' ( + ServerName nvarchar(256), + deadlock_type nvarchar(256), + event_date datetime, + database_name nvarchar(256), + spid smallint, + deadlock_group nvarchar(256), + query xml, + object_names xml, + isolation_level nvarchar(256), + owner_mode nvarchar(256), + waiter_mode nvarchar(256), + lock_mode nvarchar(256), + transaction_count bigint, + client_option_1 varchar(500), + client_option_2 varchar(500), + login_name nvarchar(256), + host_name nvarchar(256), + client_app nvarchar(1024), + wait_time bigint, + wait_resource nvarchar(max), + priority smallint, + log_used bigint, + last_tran_started datetime, + last_batch_started datetime, + last_batch_completed datetime, + transaction_name nvarchar(256), + status nvarchar(256), + owner_waiter_type nvarchar(256), + owner_activity nvarchar(256), + owner_waiter_activity nvarchar(256), + owner_merging nvarchar(256), + owner_spilling nvarchar(256), + owner_waiting_to_close nvarchar(256), + waiter_waiter_type nvarchar(256), + waiter_owner_activity nvarchar(256), + waiter_waiter_activity nvarchar(256), + waiter_merging nvarchar(256), + waiter_spilling nvarchar(256), + waiter_waiting_to_close nvarchar(256), + deadlock_graph xml + )'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*table created.*/ + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + N'.sys.objects AS o + WHERE o.type_desc = N''USER_TABLE'' + AND o.name = N''BlitzLockFindings''', + @StringToExecuteParams = + N'@r sysname OUTPUT'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; + + IF (@r IS NULL) /*if table does not exist*/ + BEGIN + SELECT + @OutputTableFindings = + QUOTENAME(N'BlitzLockFindings'), + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableFindings + + N' ( + ServerName nvarchar(256), + check_id INT, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000) + );'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; + END; - /*Grab the initial set of XML to parse*/ - WITH xml - AS ( SELECT CONVERT(XML, event_data) AS deadlock_xml - FROM sys.fn_xe_file_target_read_file(@EventSessionPath, NULL, NULL, NULL) ) - SELECT TOP ( @Top ) xml.deadlock_xml - INTO #deadlock_data - FROM xml - WHERE xml.deadlock_xml.value('(/event/@name)[1]', 'VARCHAR(256)') = 'xml_deadlock_report' - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') >= @StartDate - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') < @EndDate - ORDER BY xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') - OPTION ( RECOMPILE ); + /*create synonym for deadlockfindings.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadlockFindings' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadlockFindings; + END; - + RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableFindings; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*create synonym for deadlock table.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadLockTbl' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadLockTbl; + END; - /*Parse process and input buffer XML*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.value('@id', 'NVARCHAR(256)') AS id, - ca.dp.value('@currentdb', 'BIGINT') AS database_id, - ca.dp.value('@logused', 'BIGINT') AS log_used, - ca.dp.value('@waitresource', 'NVARCHAR(256)') AS wait_resource, - ca.dp.value('@waittime', 'BIGINT') AS wait_time, - ca.dp.value('@transactionname', 'NVARCHAR(256)') AS transaction_name, - ca.dp.value('@lasttranstarted', 'DATETIME2(7)') AS last_tran_started, - ca.dp.value('@lastbatchstarted', 'DATETIME2(7)') AS last_batch_started, - ca.dp.value('@lastbatchcompleted', 'DATETIME2(7)') AS last_batch_completed, - ca.dp.value('@lockMode', 'NVARCHAR(256)') AS lock_mode, - ca.dp.value('@trancount', 'BIGINT') AS transaction_count, - ca.dp.value('@clientapp', 'NVARCHAR(256)') AS client_app, - ca.dp.value('@hostname', 'NVARCHAR(256)') AS host_name, - ca.dp.value('@loginname', 'NVARCHAR(256)') AS login_name, - ca.dp.value('@isolationlevel', 'NVARCHAR(256)') AS isolation_level, - ca2.ib.query('.') AS input_buffer, - ca.dp.query('.') AS process_xml - INTO #deadlock_process - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process/inputbuf') AS ca2(ib) - WHERE (ca.dp.value('@currentdb', 'BIGINT') = DB_ID(@DatabaseName) OR @DatabaseName IS NULL) - AND (ca.dp.value('@clientapp', 'NVARCHAR(256)') = @AppName OR @AppName IS NULL) - AND (ca.dp.value('@hostname', 'NVARCHAR(256)') = @HostName OR @HostName IS NULL) - AND (ca.dp.value('@loginname', 'NVARCHAR(256)') = @LoginName OR @LoginName IS NULL) - OPTION ( RECOMPILE ); + RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableName; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; + END; + /* WITH ROWCOUNT doesn't work on Amazon RDS - see: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2037 */ + IF @RDS = 0 + BEGIN; + BEGIN TRY; + RAISERROR('@RDS = 0, updating #t with high row and page counts', 0, 1) WITH NOWAIT; + UPDATE STATISTICS + #t + WITH + ROWCOUNT = 9223372036854775807, + PAGECOUNT = 9223372036854775807; + END TRY + BEGIN CATCH; + /* Misleading error returned, if run without permissions to update statistics the error returned is "Cannot find object". + Catching specific error, and returning message with better info. If any other error is returned, then throw as normal */ + IF (ERROR_NUMBER() = 1088) + BEGIN; + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Cannot run UPDATE STATISTICS on a #temp table without db_owner or sysadmin permissions', 0, 1) WITH NOWAIT; + END; + ELSE + BEGIN; + THROW; + END; + END CATCH; + END; + IF @DeadlockType IS NOT NULL + BEGIN + SELECT + @DeadlockType = + CASE + WHEN LOWER(@DeadlockType) LIKE 'regular%' + THEN N'Regular Deadlock' + WHEN LOWER(@DeadlockType) LIKE N'parallel%' + THEN N'Parallel Deadlock' + ELSE NULL + END; + END; - /*Parse execution stack XML*/ - SELECT dp.id, - dp.event_date, - ca.dp.value('@procname', 'NVARCHAR(1000)') AS proc_name, - ca.dp.value('@sqlhandle', 'NVARCHAR(128)') AS sql_handle - INTO #deadlock_stack - FROM #deadlock_process AS dp - CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) - WHERE (ca.dp.value('@procname', 'NVARCHAR(256)') = @StoredProcName OR @StoredProcName IS NULL) - OPTION ( RECOMPILE ); + /*If @TargetSessionType, we need to figure out if it's ring buffer or event file*/ + /*Azure has differently named views, so we need to separate. Thanks, Azure.*/ + IF + ( + @Azure = 0 + AND @TargetSessionType IS NULL + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for non-Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_sessions AS s + JOIN sys.dm_xe_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for non-Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; + IF + ( + @Azure = 1 + AND @TargetSessionType IS NULL + AND LOWER(@TargetSessionType) <> N'table' + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_database_sessions AS s + JOIN sys.dm_xe_database_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; - /*Grab the full resource list*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.query('.') AS resource_xml - INTO #deadlock_resource - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) - OPTION ( RECOMPILE ); + /*The system health stuff gets handled different from user extended events.*/ + /*These next sections deal with user events, dependent on target.*/ - /*This parses object locks*/ - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(1000)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - INTO #deadlock_owner_waiter - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - WHERE (ca.dr.value('@objectname', 'NVARCHAR(1000)') = @ObjectName OR @ObjectName IS NULL) - OPTION ( RECOMPILE ); + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for non-Azure at %s', 0, 1, @d) WITH NOWAIT; + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_session_targets AS t + JOIN sys.dm_xe_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for Azure at %s', 0, 1, @d) WITH NOWAIT; - /*This parses page locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_database_session_targets AS t + JOIN sys.dm_xe_database_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + END; - /*This parses key locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for non-Azure', 0, 1) WITH NOWAIT; + SELECT + @SessionId = t.event_session_id, + @TargetSessionId = t.target_id + FROM sys.server_event_session_targets AS t + JOIN sys.server_event_sessions AS s + ON s.event_session_id = t.event_session_id + WHERE t.name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; - /*This parses rid locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); + IF @Azure = 1 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for Azure', 0, 1) WITH NOWAIT; + SELECT + @SessionId = + t.event_session_address, + @TargetSessionId = + t.target_name + FROM sys.dm_xe_database_session_targets t + JOIN sys.dm_xe_database_sessions s + ON s.address = t.event_session_address + WHERE t.target_name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; - /*Get rid of nonsense*/ - DELETE dow - FROM #deadlock_owner_waiter AS dow - WHERE dow.owner_id = dow.waiter_id; - - /*Add some nonsense*/ - ALTER TABLE #deadlock_process - ADD waiter_mode NVARCHAR(256), - owner_mode NVARCHAR(256), - is_victim AS CONVERT(BIT, CASE WHEN id = victim_id THEN 1 ELSE 0 END); - - /*Update some nonsense*/ - UPDATE dp - SET dp.owner_mode = dow.owner_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 0; - - UPDATE dp - SET dp.waiter_mode = dow.waiter_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.victim_id = dow.waiter_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 1; - - - /*Begin checks based on parsed values*/ - - /*Check 1 is deadlocks by database*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 1 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Total database locks' AS finding_group, - 'This database had ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) - + ' deadlocks.' - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Reading for event_file %s', 0, 1, @FileName) WITH NOWAIT; - /*Check 2 is deadlocks by object*/ - - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 2 AS check_id, - DB_NAME(dow.database_id) AS database_name, - dow.object_name AS object_name, - 'Total object deadlocks' AS finding_group, - 'This object was involved in ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.object_name)) - + ' deadlock(s).' - FROM #deadlock_owner_waiter AS dow - GROUP BY DB_NAME(dow.database_id), dow.object_name - OPTION ( RECOMPILE ); - + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(f.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(@FileName, NULL, NULL, NULL) AS f + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; - /*Check 3 looks for Serializable locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 3 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Serializable locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of serializable deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'serializable%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); + /*The XML is parsed differently if it comes from the event file or ring buffer*/ + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for ring buffer data', 0, 1) WITH NOWAIT; - /*Check 4 looks for Repeatable Read locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 4 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Repeatable Read locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of repeatable read deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'repeatable read%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query(N'.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.nodes('/RingBufferTarget/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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event_file%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for event file data', 0, 1) WITH NOWAIT; + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; - /*Check 5 breaks down app, host, and login information*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 5 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Login, App, and Host locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) + - ' instances of deadlocks involving the login ' + - ISNULL(dp.login_name, 'UNKNOWN') + - ' from the application ' + - ISNULL(dp.client_app, 'UNKNOWN') + - ' on host ' + - ISNULL(dp.host_name, 'UNKNOWN') - AS finding - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id), dp.login_name, dp.client_app, dp.host_name - OPTION ( RECOMPILE ); + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query('.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + /*This section deals with event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Grab the initial set of xml to parse at %s', 0, 1, @d) WITH NOWAIT; - /*Check 6 breaks down the types of locks (object, page, key, etc.)*/ - WITH lock_types AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - dow.object_name, - SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) -1) AS lock, - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.id)) AS lock_count - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - GROUP BY DB_NAME(dp.database_id), SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) - 1), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 6 AS check_id, - lt.database_name, - lt.object_name, - 'Types of locks by object' AS finding_group, - 'This object has had ' + - STUFF((SELECT DISTINCT N', ' + lt2.lock_count + ' ' + 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'') - + ' locks' - FROM lock_types AS lt - OPTION ( RECOMPILE ); + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + SELECT + xml.deadlock_xml + INTO #xml + FROM + ( + SELECT + deadlock_xml = + TRY_CAST(fx.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS fx + LEFT JOIN #t AS t + ON 1 = 1 + WHERE fx.object_name = N'xml_deadlock_report' + ) AS xml + CROSS APPLY xml.deadlock_xml.nodes('/event') AS e(x) + WHERE 1 = 1 + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + INSERT + #deadlock_data WITH(TABLOCKX) + SELECT + deadlock_xml = + xml.deadlock_xml + FROM #xml AS xml + LEFT JOIN #t AS t + ON 1 = 1 + WHERE xml.deadlock_xml IS NOT NULL + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; - /*Check 7 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name, - '''' + STUFF((SELECT DISTINCT N',' + ds2.sql_handle - FROM #deadlock_stack AS ds2 - WHERE ds2.id = ds.id - AND ds2.event_date = ds.event_date - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + '''' AS sql_handle_csv - FROM #deadlock_stack AS ds - GROUP BY PARSENAME(ds.proc_name, 3), - PARSENAME(ds.proc_name, 2), - PARSENAME(ds.proc_name, 1), - ds.id, - ds.proc_name, - ds.event_date - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzCache ' + - CASE WHEN ds.proc_name = 'adhoc' - THEN ' @OnlySqlHandles = ' + sql_handle_csv - ELSE '@StoredProcName = ' + - QUOTENAME(ds.proc_only_name, '''') - END + - ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - OPTION ( RECOMPILE ); + /* If table target */ + 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; + + /* + 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 ' + + 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); + '; + + IF @Debug = 1 BEGIN PRINT @extract_sql; END; - IF @ProductVersionMajor >= 13 - BEGIN - - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.sql_handle, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name - FROM #deadlock_stack AS ds - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzQueryStore ' - + '@DatabaseName = ' - + QUOTENAME(ds.database_name, '''') - + ', ' - + '@StoredProcName = ' - + QUOTENAME(ds.proc_only_name, '''') - + ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - WHERE ds.proc_name <> 'adhoc' - OPTION ( RECOMPILE ); - END; - + EXECUTE sys.sp_executesql + @extract_sql, + N' + @xe bit OUTPUT, + @xd bit OUTPUT + ', + @xe OUTPUT, + @xd OUTPUT; - /*Check 8 gives you stored proc deadlock counts*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 8 AS check_id, - DB_NAME(dp.database_id) AS database_name, - ds.proc_name, - 'Stored Procedure Deadlocks', - 'The stored procedure ' - + PARSENAME(ds.proc_name, 2) - + '.' - + PARSENAME(ds.proc_name, 1) - + ' has been involved in ' - + CONVERT(NVARCHAR(10), COUNT_BIG(DISTINCT ds.id)) - + ' deadlocks.' - FROM #deadlock_stack AS ds - JOIN #deadlock_process AS dp - ON dp.id = ds.id - AND ds.event_date = dp.event_date - WHERE ds.proc_name <> 'adhoc' - GROUP BY DB_NAME(dp.database_id), ds.proc_name - OPTION(RECOMPILE); + /* 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; - /*Check 9 gives you more info queries for sp_BlitzIndex */ - WITH bi AS ( - SELECT DISTINCT - dow.object_name, - PARSENAME(dow.object_name, 3) AS database_name, - PARSENAME(dow.object_name, 2) AS schema_name, - PARSENAME(dow.object_name, 1) AS table_name - FROM #deadlock_owner_waiter AS dow - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 9 AS check_id, - bi.database_name, - bi.schema_name + '.' + bi.table_name, - 'More Info - Table' AS finding_group, - 'EXEC sp_BlitzIndex ' + - '@DatabaseName = ' + QUOTENAME(bi.database_name, '''') + - ', @SchemaName = ' + QUOTENAME(bi.schema_name, '''') + - ', @TableName = ' + QUOTENAME(bi.table_name, '''') + - ';' AS finding - FROM bi - OPTION ( RECOMPILE ); + 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; - /*Check 10 gets total deadlock wait time per object*/ - WITH chopsuey AS ( - SELECT DISTINCT - PARSENAME(dow.object_name, 3) AS database_name, - dow.object_name, - CONVERT(VARCHAR(10), (SUM(DISTINCT dp.wait_time) / 1000) / 86400) AS wait_days, - CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT dp.wait_time) / 1000), 0), 108) AS wait_time_hms - FROM #deadlock_owner_waiter AS dow - JOIN #deadlock_process AS dp - ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - GROUP BY PARSENAME(dow.object_name, 3), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 10 AS check_id, - cs.database_name, - cs.object_name, - 'Total object deadlock wait time' AS finding_group, - 'This object has had ' - + CONVERT(VARCHAR(10), cs.wait_days) - + ':' + CONVERT(VARCHAR(20), cs.wait_time_hms, 108) - + ' [d/h/m/s] of deadlock wait time.' AS finding - FROM chopsuey AS cs - WHERE cs.object_name IS NOT NULL - OPTION ( RECOMPILE ); - - /*Check 11 gets total deadlock wait time per database*/ - WITH wait_time AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - SUM(CONVERT(BIGINT, dp.wait_time)) AS total_wait_time_ms - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 11 AS check_id, - wt.database_name, - '-' AS object_name, - 'Total database deadlock wait time' AS finding_group, - 'This database has had ' - + CONVERT(VARCHAR(10), (SUM(DISTINCT wt.total_wait_time_ms) / 1000) / 86400) - + ':' + CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT wt.total_wait_time_ms) / 1000), 0), 108) - + ' [d/h/m/s] of deadlock wait time.' - FROM wait_time AS wt - GROUP BY wt.database_name - OPTION ( RECOMPILE ); + /*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; - /*Thank you goodnight*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - VALUES ( -1, - N'sp_BlitzLock ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - N'SQL Server First Responder Kit', - N'http://FirstResponderKit.org/', - N'To get help or add your own contributions, join us at http://FirstResponderKit.org.'); + /*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; + + SELECT + d1.deadlock_xml, + event_date = d1.deadlock_xml.value('(event/@timestamp)[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('/event/data/value/deadlock') + INTO #dd + FROM #deadlock_data AS d1 + LEFT JOIN #t AS t + ON 1 = 1 + WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' - + 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); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Final Parse process and input buffer xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT + q.event_date, + q.victim_id, + is_parallel = + CONVERT(bit, q.is_parallel), + q.deadlock_graph, + q.id, + q.spid, + q.database_id, + database_name = + ISNULL + ( + DB_NAME(q.database_id), + N'UNKNOWN' + ), + q.current_database_name, + q.priority, + q.log_used, + q.wait_resource, + q.wait_time, + q.transaction_name, + q.last_tran_started, + q.last_batch_started, + q.last_batch_completed, + q.lock_mode, + q.status, + q.transaction_count, + q.client_app, + q.host_name, + q.login_name, + q.isolation_level, + client_option_1 = + SUBSTRING + ( + CASE WHEN q.clientoption1 & 1 = 1 THEN ', DISABLE_DEF_CNST_CHECK' ELSE '' END + + CASE WHEN q.clientoption1 & 2 = 2 THEN ', IMPLICIT_TRANSACTIONS' ELSE '' END + + CASE WHEN q.clientoption1 & 4 = 4 THEN ', CURSOR_CLOSE_ON_COMMIT' ELSE '' END + + CASE WHEN q.clientoption1 & 8 = 8 THEN ', ANSI_WARNINGS' ELSE '' END + + CASE WHEN q.clientoption1 & 16 = 16 THEN ', ANSI_PADDING' ELSE '' END + + CASE WHEN q.clientoption1 & 32 = 32 THEN ', ANSI_NULLS' ELSE '' END + + CASE WHEN q.clientoption1 & 64 = 64 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 128 = 128 THEN ', ARITHIGNORE' ELSE '' END + + CASE WHEN q.clientoption1 & 256 = 256 THEN ', QUOTED_IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption1 & 512 = 512 THEN ', NOCOUNT' ELSE '' END + + CASE WHEN q.clientoption1 & 1024 = 1024 THEN ', ANSI_NULL_DFLT_ON' ELSE '' END + + CASE WHEN q.clientoption1 & 2048 = 2048 THEN ', ANSI_NULL_DFLT_OFF' ELSE '' END + + CASE WHEN q.clientoption1 & 4096 = 4096 THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + + CASE WHEN q.clientoption1 & 8192 = 8192 THEN ', NUMERIC_ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 16384 = 16384 THEN ', XACT_ABORT' ELSE '' END, + 3, + 500 + ), + client_option_2 = + SUBSTRING + ( + CASE WHEN q.clientoption2 & 1024 = 1024 THEN ', DB CHAINING' ELSE '' END + + CASE WHEN q.clientoption2 & 2048 = 2048 THEN ', NUMERIC ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 4096 = 4096 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 8192 = 8192 THEN ', ANSI PADDING' ELSE '' END + + CASE WHEN q.clientoption2 & 16384 = 16384 THEN ', ANSI NULL DEFAULT' ELSE '' END + + CASE WHEN q.clientoption2 & 65536 = 65536 THEN ', CONCAT NULL YIELDS NULL' ELSE '' END + + CASE WHEN q.clientoption2 & 131072 = 131072 THEN ', RECURSIVE TRIGGERS' ELSE '' END + + CASE WHEN q.clientoption2 & 1048576 = 1048576 THEN ', DEFAULT TO LOCAL CURSOR' ELSE '' END + + CASE WHEN q.clientoption2 & 8388608 = 8388608 THEN ', QUOTED IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption2 & 16777216 = 16777216 THEN ', AUTO CREATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 33554432 = 33554432 THEN ', CURSOR CLOSE ON COMMIT' ELSE '' END + + CASE WHEN q.clientoption2 & 67108864 = 67108864 THEN ', ANSI NULLS' ELSE '' END + + CASE WHEN q.clientoption2 & 268435456 = 268435456 THEN ', ANSI WARNINGS' ELSE '' END + + CASE WHEN q.clientoption2 & 536870912 = 536870912 THEN ', FULL TEXT ENABLED' ELSE '' END + + CASE WHEN q.clientoption2 & 1073741824 = 1073741824 THEN ', AUTO UPDATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 1469283328 = 1469283328 THEN ', ALL SETTABLE OPTIONS' ELSE '' END, + 3, + 500 + ), + q.process_xml + INTO #deadlock_process + FROM + ( + SELECT + dd.deadlock_xml, + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dd.event_date + ), + dd.victim_id, + is_parallel = + CONVERT(tinyint, dd.is_parallel) + + CONVERT(tinyint, dd.is_parallel_batch), + dd.deadlock_graph, + id = ca.dp.value('@id', 'nvarchar(256)'), + spid = ca.dp.value('@spid', 'smallint'), + database_id = ca.dp.value('@currentdb', 'bigint'), + current_database_name = ca.dp.value('@currentdbname', 'nvarchar(256)'), + priority = ca.dp.value('@priority', 'smallint'), + log_used = ca.dp.value('@logused', 'bigint'), + wait_resource = ca.dp.value('@waitresource', 'nvarchar(256)'), + wait_time = ca.dp.value('@waittime', 'bigint'), + transaction_name = ca.dp.value('@transactionname', 'nvarchar(256)'), + last_tran_started = ca.dp.value('@lasttranstarted', 'datetime'), + last_batch_started = ca.dp.value('@lastbatchstarted', 'datetime'), + last_batch_completed = ca.dp.value('@lastbatchcompleted', 'datetime'), + lock_mode = ca.dp.value('@lockMode', 'nvarchar(256)'), + status = ca.dp.value('@status', 'nvarchar(256)'), + transaction_count = ca.dp.value('@trancount', 'bigint'), + client_app = ca.dp.value('@clientapp', 'nvarchar(1024)'), + host_name = ca.dp.value('@hostname', 'nvarchar(256)'), + login_name = ca.dp.value('@loginname', 'nvarchar(256)'), + isolation_level = ca.dp.value('@isolationlevel', 'nvarchar(256)'), + clientoption1 = ca.dp.value('@clientoption1', 'bigint'), + clientoption2 = ca.dp.value('@clientoption2', 'bigint'), + process_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #dd AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) + WHERE (ca.dp.exist('@currentdb[. = sql:variable("@DatabaseId")]') = 1 OR @DatabaseName IS NULL) + AND (ca.dp.exist('@clientapp[. = sql:variable("@AppName")]') = 1 OR @AppName IS NULL) + AND (ca.dp.exist('@hostname[. = sql:variable("@HostName")]') = 1 OR @HostName IS NULL) + AND (ca.dp.exist('@loginname[. = sql:variable("@LoginName")]') = 1 OR @LoginName IS NULL) + ) AS q + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse execution stack xml*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse execution stack xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + dp.id, + dp.event_date, + proc_name = ca.dp.value('@procname', 'nvarchar(1024)'), + sql_handle = ca.dp.value('@sqlhandle', 'nvarchar(131)') + INTO #deadlock_stack + FROM #deadlock_process AS dp + CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) + WHERE (ca.dp.exist('@procname[. = sql:variable("@StoredProcName")]') = 1 OR @StoredProcName IS NULL) + AND ca.dp.exist('@sqlhandle[ .= "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"]') = 0 + OPTION(RECOMPILE); - /*Results*/ - WITH deadlocks - AS ( SELECT dp.event_date, - dp.id, - dp.victim_id, - dp.database_id, - dp.log_used, - dp.wait_resource, - CONVERT( - XML, - STUFF(( SELECT DISTINCT NCHAR(10) - + N' ' - + ISNULL(c.object_name, N'') - + N' ' AS object_name - FROM #deadlock_owner_waiter AS c - WHERE (dp.id = c.owner_id - OR dp.victim_id = c.waiter_id) - AND dp.event_date = c.event_date - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), - 1, 1, N'')) AS object_names, - dp.wait_time, - dp.transaction_name, - dp.last_tran_started, - dp.last_batch_started, - dp.last_batch_completed, - dp.lock_mode, - dp.transaction_count, - dp.client_app, - dp.host_name, - dp.login_name, - dp.isolation_level, - dp.process_xml.value('(//process/inputbuf/text())[1]', 'NVARCHAR(MAX)') AS inputbuf, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date ) AS dn, - DENSE_RANK() OVER ( ORDER BY dp.event_date ) AS en, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date ORDER BY dp.event_date ) -1 AS qn, - dp.is_victim, - ISNULL(dp.owner_mode, '-') AS owner_mode, - ISNULL(dp.waiter_mode, '-') AS waiter_mode - FROM #deadlock_process AS dp ) - SELECT d.event_date, - DB_NAME(d.database_id) AS database_name, - 'Deadlock #' - + CONVERT(NVARCHAR(10), d.en) - + ', Query #' - + CASE WHEN d.qn = 0 THEN N'1' ELSE CONVERT(NVARCHAR(10), d.qn) END - + CASE WHEN d.is_victim = 1 THEN ' - VICTIM' ELSE '' END - AS deadlock_group, - CONVERT(XML, N'' + d.inputbuf + N'') AS query, - d.object_names, - d.isolation_level, - d.owner_mode, - d.waiter_mode, - d.transaction_count, - d.login_name, - d.host_name, - d.client_app, - d.wait_time, - d.log_used, - d.last_tran_started, - d.last_batch_started, - d.last_batch_completed, - d.transaction_name - FROM deadlocks AS d - WHERE d.dn = 1 - ORDER BY d.event_date, is_victim DESC; - - - - SELECT df.check_id, df.database_name, df.object_name, df.finding_group, df.finding - FROM #deadlock_findings AS df - ORDER BY df.check_id - OPTION ( RECOMPILE ); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + /*Grab the full resource list*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); - IF @Debug = 1 - BEGIN + RAISERROR('Grab the full resource list %s', 0, 1, @d) WITH NOWAIT; - SELECT '#deadlock_data' AS table_name, * - FROM #deadlock_data AS dd - OPTION ( RECOMPILE ); + SELECT + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dr.event_date + ), + dr.victim_id, + dr.resource_xml + INTO + #deadlock_resource + FROM + ( + SELECT + event_date = dd.deadlock_xml.value('(event/@timestamp)[1]', 'datetime2'), + victim_id = dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'nvarchar(256)'), + resource_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #deadlock_data AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) + ) AS dr + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse object locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse object locks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + object_name = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + ca.object_name COLLATE Latin1_General_BIN2, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?'), + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = CAST(N'OBJECT' AS nvarchar(100)) + INTO #deadlock_owner_waiter + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse page locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse page locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'PAGE' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse key locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse key locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'KEY' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse RID locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse RID locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'RID' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse row group locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse row group locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'ROWGROUP' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/rowgrouplock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Fixing heaps in #deadlock_owner_waiter %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + d + SET + d.index_name = + d.object_name + N'.HEAP' + FROM #deadlock_owner_waiter AS d + WHERE d.lock_type IN + ( + N'HEAP', + N'RID' + ) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.id, + ca.event_date, + ca.wait_type, + ca.node_id, + ca.waiter_type, + ca.owner_activity, + ca.waiter_activity, + ca.merging, + ca.spilling, + ca.waiting_to_close, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)') + INTO #deadlock_resource_parallel + FROM + ( + SELECT + dr.event_date, + id = ca.dr.value('@id', 'nvarchar(256)'), + wait_type = ca.dr.value('@WaitType', 'nvarchar(256)'), + node_id = ca.dr.value('@nodeId', 'bigint'), + /* These columns are in 2017 CU5+ ONLY */ + waiter_type = ca.dr.value('@waiterType', 'nvarchar(256)'), + owner_activity = ca.dr.value('@ownerActivity', 'nvarchar(256)'), + waiter_activity = ca.dr.value('@waiterActivity', 'nvarchar(256)'), + merging = ca.dr.value('@merging', 'nvarchar(256)'), + spilling = ca.dr.value('@spilling', 'nvarchar(256)'), + waiting_to_close = ca.dr.value('@waitingToClose', 'nvarchar(256)'), + /* These columns are in 2017 CU5+ ONLY */ + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/exchangeEvent') AS ca(dr) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); - SELECT '#deadlock_resource' AS table_name, * - FROM #deadlock_resource AS dr - OPTION ( RECOMPILE ); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - SELECT '#deadlock_owner_waiter' AS table_name, * - FROM #deadlock_owner_waiter AS dow - OPTION ( RECOMPILE ); + /*Get rid of parallel noise*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of parallel noise %s', 0, 1, @d) WITH NOWAIT; - SELECT '#deadlock_process' AS table_name, * - FROM #deadlock_process AS dp - OPTION ( RECOMPILE ); + WITH + c AS + ( + SELECT + *, + rn = + ROW_NUMBER() OVER + ( + PARTITION BY + drp.owner_id, + drp.waiter_id + ORDER BY + drp.event_date + ) + FROM #deadlock_resource_parallel AS drp + ) + DELETE + FROM c + WHERE c.rn > 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get rid of nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of nonsense %s', 0, 1, @d) WITH NOWAIT; + + DELETE dow + FROM #deadlock_owner_waiter AS dow + WHERE dow.owner_id = dow.waiter_id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Add some nonsense*/ + ALTER TABLE + #deadlock_process + ADD + waiter_mode nvarchar(256), + owner_mode nvarchar(256), + is_victim AS + CONVERT + ( + bit, + CASE + WHEN id = victim_id + THEN 1 + ELSE 0 + END + ) PERSISTED; + + /*Update some nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 1 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.owner_mode = dow.owner_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.id = dow.owner_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 0 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 2 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.waiter_mode = dow.waiter_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.victim_id = dow.waiter_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 1 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get Agent Job and Step names*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get Agent Job and Step names %s', 0, 1, @d) WITH NOWAIT; + + SELECT + x.event_date, + x.victim_id, + x.id, + x.database_id, + x.client_app, + x.job_id, + x.step_id, + job_id_guid = + CONVERT + ( + uniqueidentifier, + TRY_CAST + ( + N'' + AS xml + ).value('xs:hexBinary(substring(sql:column("x.job_id"), 0))', 'binary(16)') + ) + INTO #agent_job + FROM + ( + SELECT + dp.event_date, + dp.victim_id, + dp.id, + dp.database_id, + dp.client_app, + job_id = + SUBSTRING + ( + dp.client_app, + CHARINDEX(N'0x', dp.client_app) + LEN(N'0x'), + 32 + ), + step_id = + CASE + WHEN CHARINDEX(N': Step ', dp.client_app) > 0 + AND CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) > 0 + THEN + SUBSTRING + ( + dp.client_app, + CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step '), + CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) - + (CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step ')) + ) + ELSE dp.client_app + END + FROM #deadlock_process AS dp + WHERE dp.client_app LIKE N'SQLAgent - %' + AND dp.client_app <> N'SQLAgent - Initial Boot Probe' + ) AS x + OPTION(RECOMPILE); + + ALTER TABLE + #agent_job + ADD + job_name nvarchar(256), + step_name nvarchar(256); + + IF + ( + @Azure = 0 + AND @RDS = 0 + ) + BEGIN + SET @StringToExecute = + N' + UPDATE + aj + SET + aj.job_name = j.name, + aj.step_name = s.step_name + FROM msdb.dbo.sysjobs AS j + JOIN msdb.dbo.sysjobsteps AS s + ON j.job_id = s.job_id + JOIN #agent_job AS aj + ON aj.job_id_guid = j.job_id + AND aj.step_id = s.step_id + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; - SELECT '#deadlock_stack' AS table_name, * - FROM #deadlock_stack AS ds - OPTION ( RECOMPILE ); - - END; -- End debug + END; - END; --Final End + UPDATE + dp + SET + dp.client_app = + CASE + WHEN dp.client_app LIKE N'SQLAgent - %' + THEN N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name + ELSE dp.client_app + END + FROM #deadlock_process AS dp + JOIN #agent_job AS aj + ON dp.event_date = aj.event_date + AND dp.victim_id = aj.victim_id + AND dp.id = aj.id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get each and every table of all databases*/ + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId %s', 0, 1, @d) WITH NOWAIT; -GO + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + EXECUTE sys.sp_MSforeachdb + N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO + USE [?]; -DECLARE @msg NVARCHAR(MAX) = N''; + IF EXISTS + ( + SELECT + 1/0 + FROM #deadlock_process AS dp + WHERE dp.database_id = DB_ID() + ) + BEGIN + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); + END; + '; - -- Must be a compatible, on-prem version of SQL (2016+) -IF ( (SELECT SERVERPROPERTY ('EDITION')) <> 'SQL Azure' - AND (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) < 13 - ) - -- or Azure Database (not Azure Data Warehouse), running at database compat level 130+ -OR ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' - AND (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 - AND (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 - ) -BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on versions of SQL prior to 2016, or Azure Database compatibility < 130.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; -END; + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; -IF OBJECT_ID('dbo.sp_BlitzQueryStore') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzQueryStore AS RETURN 0;'); -GO + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId at %s', 0, 1, @d) WITH NOWAIT; -ALTER PROCEDURE dbo.sp_BlitzQueryStore - @Help BIT = 0, - @DatabaseName NVARCHAR(128) = NULL , - @Top INT = 3, - @StartDate DATETIME2 = NULL, - @EndDate DATETIME2 = NULL, - @MinimumExecutionCount INT = NULL, - @DurationFilter DECIMAL(38,4) = NULL , - @StoredProcName NVARCHAR(128) = NULL, - @Failed BIT = 0, - @PlanIdFilter INT = NULL, - @QueryIdFilter INT = NULL, - @ExportToExcel BIT = 0, - @HideSummary BIT = 0 , - @SkipXML BIT = 0, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -BEGIN /*First BEGIN*/ + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + /*Begin checks based on parsed values*/ -DECLARE @Version NVARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - -DECLARE /*Variables for the variable Gods*/ - @msg NVARCHAR(MAX) = N'', --Used to format RAISERROR messages in some places - @sql_select NVARCHAR(MAX) = N'', --Used to hold SELECT statements for dynamic SQL - @sql_where NVARCHAR(MAX) = N'', -- Used to hold WHERE clause for dynamic SQL - @duration_filter_ms DECIMAL(38,4) = (@DurationFilter * 1000.), --We accept Duration in seconds, but we filter in milliseconds (this is grandfathered from sp_BlitzCache) - @execution_threshold INT = 1000, --Threshold at which we consider a query to be frequently executed - @ctp_threshold_pct TINYINT = 10, --Percentage of CTFP at which we consider a query to be near parallel - @long_running_query_warning_seconds BIGINT = 300 * 1000 ,--Number of seconds (converted to milliseconds) at which a query is considered long running - @memory_grant_warning_percent INT = 10,--Percent of memory grant used compared to what's granted; used to trigger unused memory grant warning - @ctp INT,--Holds the CTFP value for the server - @min_memory_per_query INT,--Holds the server configuration value for min memory per query - @cr NVARCHAR(1) = NCHAR(13),--Special character - @lf NVARCHAR(1) = NCHAR(10),--Special character - @tab NVARCHAR(1) = NCHAR(9),--Special character - @error_severity INT,--Holds error info for try/catch blocks - @error_state INT,--Holds error info for try/catch blocks - @sp_params NVARCHAR(MAX) = N'@sp_Top INT, @sp_StartDate DATETIME2, @sp_EndDate DATETIME2, @sp_MinimumExecutionCount INT, @sp_MinDuration INT, @sp_StoredProcName NVARCHAR(128), @sp_PlanIdFilter INT, @sp_QueryIdFilter INT',--Holds parameters used in dynamic SQL - @is_azure_db BIT = 0, --Are we using Azure? I'm not. You might be. That's cool. - @compatibility_level TINYINT = 0, --Some functionality (T-SQL) isn't available in lower compat levels. We can use this to weed out those issues as we go. - @log_size_mb DECIMAL(38,2) = 0, - @avg_tempdb_data_file DECIMAL(38,2) = 0; - -/*Grabs CTFP setting*/ -SELECT @ctp = NULLIF(CAST(value AS INT), 0) -FROM sys.configurations -WHERE name = N'cost threshold for parallelism' -OPTION (RECOMPILE); + /* + First, revert these back since we already converted the event data to local time, + and searches will break if we use the times converted over to UTC for the event data + */ + SELECT + @StartDate = @StartDateOriginal, + @EndDate = @EndDateOriginal; -/*Grabs min query memory setting*/ -SELECT @min_memory_per_query = CONVERT(INT, c.value) -FROM sys.configurations AS c -WHERE c.name = N'min memory per query (KB)' -OPTION (RECOMPILE); + /*Check 1 is deadlocks by database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 1 database deadlocks %s', 0, 1, @d) WITH NOWAIT; -/*Grabs log size for datbase*/ -SELECT @log_size_mb = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID(@DatabaseName) -AND mf.type_desc = 'LOG'; + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 1, + dp.database_name, + object_name = N'-', + finding_group = N'Total Database Deadlocks', + finding = + N'This database had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 2 is deadlocks with selects*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 2 select deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 2, + dow.database_name, + object_name = + CASE + WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name COLLATE DATABASE_DEFAULT = dow.database_name COLLATE DATABASE_DEFAULT + AND d.is_read_committed_snapshot_on = 1 + ) + THEN N'You already enabled RCSI, but...' + ELSE N'You Might Need RCSI' + END, + finding_group = N'Total Deadlocks Involving Selects', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s) between read queries and modification queries.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND dow.lock_mode IN + ( + N'S', + N'IS' + ) + OR dow.owner_mode IN + ( + N'S', + N'IS' + ) + OR dow.waiter_mode IN + ( + N'S', + N'IS' + ) + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 is deadlocks by object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 object deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + object_name = + ISNULL + ( + dow.object_name, + N'UNKNOWN' + ), + finding_group = N'Total Object Deadlocks', + finding = + N'This object was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name, + dow.object_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 continuation, number of deadlocks per index*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) index deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Index Deadlocks', + finding = + N'This index was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type NOT IN + ( + N'HEAP', + N'RID' + ) + AND dow.index_name IS NOT NULL + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); -/*Grab avg tempdb file size*/ -SELECT @avg_tempdb_data_file = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID('tempdb') -AND mf.type_desc = 'ROWS'; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + /*Check 3 continuation, number of deadlocks per heap*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) heap deadlocks %s', 0, 1, @d) WITH NOWAIT; -/*Help section*/ + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Heap Deadlocks', + finding = + N'This heap was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type IN + ( + N'HEAP', + N'RID' + ) + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); -IF @Help = 1 - BEGIN - - SELECT N'You have requested assistance. It will arrive as soon as humanly possible.' AS [Take four red capsules, help is on the way]; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - PRINT N' - sp_BlitzQueryStore from http://FirstResponderKit.org - - This script displays your most resource-intensive queries from the Query Store, - and points to ways you can tune these queries to make them faster. - - - 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. - - Known limitations of this version: - - This query will not run on SQL Server versions less than 2016. - - This query will not run on Azure Databases with compatibility less than 130. - - This query will not run on Azure Data Warehouse. + /*Check 4 looks for Serializable deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 4 serializable deadlocks %s', 0, 1, @d) WITH NOWAIT; - Unknown limitations of this version: - - Could be tickling - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - - MIT License - - Copyright (c) 2016 Brent Ozar Unlimited - - 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. - '; - RETURN; + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 4, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Serializable Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Serializable deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'serializable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 5 looks for Repeatable Read deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 5 repeatable read deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 5, + dp.database_name, + object_name = N'-', + finding_group = N'Repeatable Read Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Repeatable Read deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'repeatable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 6 breaks down app, host, and login information*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 6 app/host/login deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 6, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Login, App, and Host deadlocks', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of deadlocks involving the login ' + + ISNULL + ( + dp.login_name, + N'UNKNOWN' + ) + + N' from the application ' + + ISNULL + ( + dp.client_app, + N'UNKNOWN' + ) + + N' on host ' + + ISNULL + ( + dp.host_name, + N'UNKNOWN' + ) + + N'.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY + dp.database_name, + dp.login_name, + dp.client_app, + dp.host_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 7 breaks down the types of deadlocks (object, page, key, etc.)*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 7 types of deadlocks %s', 0, 1, @d) WITH NOWAIT; + + WITH + lock_types AS + ( + SELECT + 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 + ) + ELSE dp.wait_resource + END, + 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 + GROUP BY + dp.database_name, + CASE + WHEN CHARINDEX(N':', dp.wait_resource) > 0 + THEN SUBSTRING + ( + dp.wait_resource, + 1, + CHARINDEX(N':', dp.wait_resource) - 1 + ) + ELSE dp.wait_resource + END, + dow.object_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 7, + lt.database_name, + lt.object_name, + finding_group = N'Types of locks by object', + finding = + N'This object has had ' + + STUFF + ( + ( + SELECT + 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'' + ) + N' locks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) + FROM lock_types AS lt + OPTION(RECOMPILE); -END; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; -/*Making sure your version is copasetic*/ -IF ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' ) - BEGIN - SET @is_azure_db = 1; + /*Check 8 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 1 BlitzCache %s', 0, 1, @d) WITH NOWAIT; - IF ( (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 - OR (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 - ) - BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on Azure Data Warehouse, or Azure Databases with DB compatibility < 130.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; - END; -ELSE IF ( (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4) ) < 13 ) - BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on versions of SQL prior to 2016.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.event_date, + ds.proc_name, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1), + sql_handle_csv = + N'''' + + STUFF + ( + ( + SELECT DISTINCT + N',' + + ds2.sql_handle + FROM #deadlock_stack AS ds2 + WHERE ds2.id = ds.id + AND ds2.event_date = ds.event_date + AND ds2.sql_handle <> 0x + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(MAX)'), + 1, + 1, + N'' + ) + + N'''' + FROM #deadlock_stack AS ds + WHERE ds.sql_handle <> 0x + GROUP BY + PARSENAME(ds.proc_name, 3), + PARSENAME(ds.proc_name, 2), + PARSENAME(ds.proc_name, 1), + ds.proc_name, + ds.id, + ds.event_date + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = N'EXECUTE sp_BlitzCache ' + + CASE + WHEN ds.proc_name = N'adhoc' + THEN N'@OnlySqlHandles = ' + ds.sql_handle_csv + ELSE N'@StoredProcName = ' + QUOTENAME(ds.proc_only_name, N'''') + END + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + AND ds.proc_name NOT LIKE 'Unknown%' + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + IF (@ProductVersionMajor >= 13 OR @Azure = 1) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 2 BlitzQueryStore %s', 0, 1, @d) WITH NOWAIT; -/*Making sure at least one database uses QS*/ -IF ( SELECT COUNT(*) - FROM sys.databases AS d - WHERE d.is_query_store_on = 1 - AND d.user_access_desc='MULTI_USER' - AND d.state_desc = 'ONLINE' - AND d.name NOT IN ('master', 'model', 'msdb', 'tempdb', '32767') - AND d.is_distributor = 0 ) = 0 - BEGIN - SELECT @msg = N'You don''t currently have any databases with Query Store enabled.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.sql_handle, + ds.proc_name, + ds.event_date, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1) + FROM #deadlock_stack AS ds + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = + N'EXECUTE sp_BlitzQueryStore ' + + N'@DatabaseName = ' + + QUOTENAME(ds.database_name, N'''') + + N', ' + + N'@StoredProcName = ' + + QUOTENAME(ds.proc_only_name, N'''') + + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE ds.proc_name <> N'adhoc' + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; -/*Making sure your databases are using QDS.*/ -RAISERROR('Checking database validity', 0, 1) WITH NOWAIT; + /*Check 9 gives you stored procedure deadlock counts*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 9 stored procedure deadlocks %s', 0, 1, @d) WITH NOWAIT; -IF (@is_azure_db = 1) - SET @DatabaseName = DB_NAME(); -ELSE -BEGIN + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 9, + database_name = + dp.database_name, + object_name = ds.proc_name, + finding_group = N'Stored Procedure Deadlocks', + finding = + N'The stored procedure ' + + PARSENAME(ds.proc_name, 2) + + N'.' + + PARSENAME(ds.proc_name, 1) + + N' has been involved in ' + + CONVERT + ( + nvarchar(10), + COUNT_BIG(DISTINCT ds.id) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT ds.id) DESC) + FROM #deadlock_stack AS ds + JOIN #deadlock_process AS dp + ON dp.id = ds.id + AND ds.event_date = dp.event_date + WHERE ds.proc_name <> N'adhoc' + AND (ds.proc_name = @StoredProcName OR @StoredProcName IS NULL) + 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) + GROUP BY + dp.database_name, + ds.proc_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 10 gives you more info queries for sp_BlitzIndex */ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 10 more info, BlitzIndex %s', 0, 1, @d) WITH NOWAIT; + + WITH + bi AS + ( + SELECT DISTINCT + dow.object_name, + dow.database_name, + schema_name = s.schema_name, + table_name = s.table_name + FROM #deadlock_owner_waiter AS dow + JOIN @sysAssObjId AS s + ON s.database_id = dow.database_id + AND s.partition_id = dow.associatedObjectId + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 10, + bi.database_name, + bi.object_name, + finding_group = N'More Info - Table', + finding = + N'EXECUTE sp_BlitzIndex ' + + N'@DatabaseName = ' + + QUOTENAME(bi.database_name, N'''') + + N', @SchemaName = ' + + QUOTENAME(bi.schema_name, N'''') + + N', @TableName = ' + + QUOTENAME(bi.table_name, N'''') + + N';' + FROM bi + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 11 gets total deadlock wait time per object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 11 deadlock wait time per object %s', 0, 1, @d) WITH NOWAIT; + + WITH + chopsuey AS + ( - /*If we're on Azure we don't need to check all this @DatabaseName stuff...*/ + SELECT + database_name = + dp.database_name, + dow.object_name, + wait_days = + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) / 1000 / 86400 + ) + ), + wait_time_hms = + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + ), + 0 + ), + 14 + ) + END, + total_waits = + SUM(CONVERT(bigint, dp.wait_time)) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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 (dow.object_name = @ObjectName OR @ObjectName 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) + GROUP BY + dp.database_name, + dow.object_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 11, + cs.database_name, + cs.object_name, + finding_group = N'Total object deadlock wait time', + finding = + N'This object has had ' + + CONVERT + ( + nvarchar(30), + cs.wait_days + ) + + N' ' + + CONVERT + ( + nvarchar(30), + cs.wait_time_hms, + 14 + ) + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY cs.total_waits DESC) + FROM chopsuey AS cs + WHERE cs.object_name IS NOT NULL + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 12 gets total deadlock wait time per database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 12 deadlock wait time per database %s', 0, 1, @d) WITH NOWAIT; + + WITH + wait_time AS + ( + SELECT + database_name = + dp.database_name, + total_wait_time_ms = + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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) + GROUP BY + dp.database_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 12, + wt.database_name, + object_name = N'-', + finding_group = N'Total database deadlock wait time', + N'This database has had ' + + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) / 1000 / 86400 + ) + ) + + N' ' + + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + ), + 0 + ), + 14 + ) END + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY SUM(CONVERT(bigint, wt.total_wait_time_ms)) DESC) + FROM wait_time AS wt + GROUP BY + wt.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 13 gets total deadlock wait time for SQL Agent*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 13 deadlock count for SQL Agent %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 13, + database_name = + DB_NAME(aj.database_id), + object_name = + N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name, + finding_group = N'Agent Job Deadlocks', + finding = + N'There have been ' + + RTRIM(COUNT_BIG(DISTINCT aj.event_date)) + + N' deadlocks from this Agent Job and Step.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT aj.event_date) DESC) + FROM #agent_job AS aj + GROUP BY + DB_NAME(aj.database_id), + aj.job_name, + aj.step_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 14 is total parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 14 parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total parallel deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT drp.event_date) + ) + + N' parallel deadlocks.' + FROM #deadlock_resource_parallel AS drp + HAVING COUNT_BIG(DISTINCT drp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 15 is total deadlocks involving sleeping sessions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 15 sleeping and background deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving sleeping sessions', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' sleepy deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.status = N'sleeping' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving background processes', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks with background task.' + FROM #deadlock_process AS dp + WHERE dp.status = N'background' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 16 is total deadlocks involving implicit transactions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 16 implicit transaction deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total implicit transaction deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' implicit transaction deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.transaction_name = N'implicit_transaction' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Thank you goodnight*/ + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + VALUES + ( + -1, + N'sp_BlitzLock version ' + CONVERT(nvarchar(10), @Version), + N'Results for ' + CONVERT(nvarchar(10), @StartDate, 23) + N' through ' + CONVERT(nvarchar(10), @EndDate, 23), + N'http://FirstResponderKit.org/', + N'To get help or add your own contributions to the SQL Server First Responder Kit, join us at http://FirstResponderKit.org.' + ); - SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)); + RAISERROR('Finished rollup at %s', 0, 1, @d) WITH NOWAIT; - /*Did you set @DatabaseName?*/ - RAISERROR('Making sure [%s] isn''t NULL', 0, 1, @DatabaseName) WITH NOWAIT; - IF (@DatabaseName IS NULL) - BEGIN - RAISERROR('@DatabaseName cannot be NULL', 0, 1) WITH NOWAIT; - RETURN; - END; + /*Results*/ + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results 1 %s', 0, 1, @d) WITH NOWAIT; - /*Does the database exist?*/ - RAISERROR('Making sure [%s] exists', 0, 1, @DatabaseName) WITH NOWAIT; - IF ((DB_ID(@DatabaseName)) IS NULL) - BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) does not exist. Please check the name and try again.', 0, 1, @DatabaseName) WITH NOWAIT; - RETURN; - END; + CREATE CLUSTERED INDEX cx_whatever_dp ON #deadlock_process (event_date, id); + CREATE CLUSTERED INDEX cx_whatever_drp ON #deadlock_resource_parallel (event_date, owner_id); + CREATE CLUSTERED INDEX cx_whatever_dow ON #deadlock_owner_waiter (event_date, owner_id, waiter_id); - /*Is it online?*/ - RAISERROR('Making sure [%s] is online', 0, 1, @DatabaseName) WITH NOWAIT; - IF (DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' - BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) is not readable. Please check the name and try again. Better yet, check your server.', 0, 1, @DatabaseName); - RETURN; - END; -END; + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; -/*Does it have Query Store enabled?*/ -RAISERROR('Making sure [%s] has Query Store enabled', 0, 1, @DatabaseName) WITH NOWAIT; -IF - ((DB_ID(@DatabaseName)) IS NOT NULL AND @DatabaseName <> '') -AND - ( SELECT DB_NAME(d.database_id) - FROM sys.databases AS d - WHERE d.is_query_store_on = 1 - AND d.user_access_desc='MULTI_USER' - AND d.state_desc = 'ONLINE' - AND DB_NAME(d.database_id) = @DatabaseName ) IS NULL -BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) does not have the Query Store enabled. Please check the name or settings, and try again.', 0, 1, @DatabaseName) WITH NOWAIT; - RETURN; -END; + WITH + deadlocks AS + ( + SELECT + deadlock_type = + N'Regular Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + wait_resource = + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + dp.is_victim, + owner_mode = + ISNULL(dp.owner_mode, N'-'), + owner_waiter_type = NULL, + owner_activity = NULL, + owner_waiter_activity = NULL, + owner_merging = NULL, + owner_spilling = NULL, + owner_waiting_to_close = NULL, + waiter_mode = + ISNULL(dp.waiter_mode, N'-'), + waiter_waiter_type = NULL, + waiter_owner_activity = NULL, + waiter_waiter_activity = NULL, + waiter_merging = NULL, + waiter_spilling = NULL, + waiter_waiting_to_close = NULL, + dp.deadlock_graph + FROM #deadlock_process AS dp + WHERE dp.victim_id IS NOT NULL + AND dp.is_parallel = 0 -/*Check database compat level*/ + UNION ALL -RAISERROR('Checking database compatibility level', 0, 1) WITH NOWAIT; + SELECT + deadlock_type = + N'Parallel Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + is_victim = 1, + owner_mode = cao.wait_type COLLATE DATABASE_DEFAULT, + owner_waiter_type = cao.waiter_type, + owner_activity = cao.owner_activity, + owner_waiter_activity = cao.waiter_activity, + owner_merging = cao.merging, + owner_spilling = cao.spilling, + owner_waiting_to_close = cao.waiting_to_close, + waiter_mode = caw.wait_type COLLATE DATABASE_DEFAULT, + waiter_waiter_type = caw.waiter_type, + waiter_owner_activity = caw.owner_activity, + waiter_waiter_activity = caw.waiter_activity, + waiter_merging = caw.merging, + waiter_spilling = caw.spilling, + waiter_waiting_to_close = caw.waiting_to_close, + dp.deadlock_graph + FROM #deadlock_process AS dp + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeNewRow' + ) + ORDER BY drp.event_date + ) AS cao + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeGetRow' + ) + ORDER BY drp.event_date + ) AS caw + WHERE dp.is_parallel = 1 + ) + SELECT + d.deadlock_type, + d.event_date, + d.id, + d.victim_id, + d.spid, + deadlock_group = + N'Deadlock #' + + CONVERT + ( + nvarchar(10), + d.en + ) + + N', Query #' + + CASE + WHEN d.qn = 0 + THEN N'1' + ELSE CONVERT(nvarchar(10), d.qn) + END + CASE + WHEN d.is_victim = 1 + THEN N' - VICTIM' + ELSE N'' + END, + d.database_id, + d.database_name, + d.current_database_name, + d.priority, + d.log_used, + d.wait_resource, + d.object_names, + d.wait_time, + d.transaction_name, + d.status, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.lock_mode, + d.transaction_count, + d.client_app, + d.host_name, + d.login_name, + d.isolation_level, + d.client_option_1, + d.client_option_2, + inputbuf = + CASE + WHEN d.inputbuf + LIKE @inputbuf_bom + N'Proc |[Database Id = %' ESCAPE N'|' + THEN + OBJECT_SCHEMA_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + + N'.' + + OBJECT_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + ELSE d.inputbuf + END COLLATE Latin1_General_BIN2, + d.owner_mode, + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_mode, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + d.deadlock_graph, + d.is_victim + INTO #deadlocks + FROM deadlocks AS d + WHERE d.dn = 1 + AND (d.is_victim = @VictimsOnly + OR @VictimsOnly = 0) + AND d.qn < CASE + WHEN d.deadlock_type = N'Parallel Deadlock' + THEN 2 + ELSE 2147483647 + END + AND (DB_NAME(d.database_id) = @DatabaseName OR @DatabaseName IS NULL) + AND (d.event_date >= @StartDate OR @StartDate IS NULL) + AND (d.event_date < @EndDate OR @EndDate IS NULL) + AND (CONVERT(nvarchar(MAX), d.object_names) COLLATE Latin1_General_BIN2 LIKE N'%' + @ObjectName + N'%' OR @ObjectName IS NULL) + AND (d.client_app = @AppName OR @AppName IS NULL) + AND (d.host_name = @HostName OR @HostName IS NULL) + AND (d.login_name = @LoginName OR @LoginName IS NULL) + AND (d.deadlock_type = @DeadlockType OR @DeadlockType IS NULL) + OPTION (RECOMPILE, LOOP JOIN, HASH JOIN); + + UPDATE d + SET d.inputbuf = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + d.inputbuf, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?') + FROM #deadlocks AS d + OPTION(RECOMPILE); -SELECT @compatibility_level = d.compatibility_level -FROM sys.databases AS d -WHERE d.name = @DatabaseName; + SELECT + d.deadlock_type, + d.event_date, + database_name = + DB_NAME(d.database_id), + database_name_x = + d.database_name, + d.current_database_name, + d.spid, + d.deadlock_group, + d.client_option_1, + d.client_option_2, + d.lock_mode, + query_xml = + ( + SELECT + [processing-instruction(query)] = + d.inputbuf + FOR XML + PATH(N''), + TYPE + ), + query_string = + d.inputbuf, + d.object_names, + d.isolation_level, + d.owner_mode, + d.waiter_mode, + d.transaction_count, + d.login_name, + d.host_name, + d.client_app, + d.wait_time, + d.wait_resource, + d.priority, + d.log_used, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.transaction_name, + d.status, + /*These columns will be NULL for regular (non-parallel) deadlocks*/ + parallel_deadlock_details = + ( + SELECT + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close + FOR XML + PATH('parallel_deadlock_details'), + TYPE + ), + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + /*end parallel deadlock columns*/ + d.deadlock_graph, + d.is_victim, + d.id + INTO #deadlock_results + FROM #deadlocks AS d; + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*There's too much risk of errors sending the*/ + IF @OutputDatabaseCheck = 0 + BEGIN + SET @ExportToExcel = 0; + END; -RAISERROR('The @DatabaseName you specified ([%s])is running in compatibility level ([%d]).', 0, 1, @DatabaseName, @compatibility_level) WITH NOWAIT; + SET @deadlock_result += N' + SELECT + server_name = + @@SERVERNAME, + dr.deadlock_type, + dr.event_date, + database_name = + COALESCE + ( + dr.database_name, + dr.database_name_x, + dr.current_database_name + ), + dr.spid, + dr.deadlock_group, + ' + CASE @ExportToExcel + WHEN 1 + THEN N' + query = dr.query_string, + object_names = + REPLACE( + REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.object_names + ) COLLATE Latin1_General_BIN2, + '''', ''''), + '''', ''''),' + ELSE N'query = dr.query_xml, + dr.object_names,' + END + N' + dr.isolation_level, + dr.owner_mode, + dr.waiter_mode, + dr.lock_mode, + dr.transaction_count, + dr.client_option_1, + dr.client_option_2, + dr.login_name, + dr.host_name, + dr.client_app, + dr.wait_time, + dr.wait_resource, + dr.priority, + dr.log_used, + dr.last_tran_started, + dr.last_batch_started, + dr.last_batch_completed, + dr.transaction_name, + dr.status,' + + CASE + WHEN (@ExportToExcel = 1 + OR @OutputDatabaseCheck = 0) + THEN N' + dr.owner_waiter_type, + dr.owner_activity, + dr.owner_waiter_activity, + dr.owner_merging, + dr.owner_spilling, + dr.owner_waiting_to_close, + dr.waiter_waiter_type, + dr.waiter_owner_activity, + dr.waiter_waiter_activity, + dr.waiter_merging, + dr.waiter_spilling, + dr.waiter_waiting_to_close,' + ELSE N' + dr.parallel_deadlock_details,' + END + + CASE + @ExportToExcel + WHEN 1 + THEN N' + deadlock_graph = + REPLACE(REPLACE( + REPLACE(REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.deadlock_graph + ) COLLATE Latin1_General_BIN2, + ''NCHAR(10)'', ''''), ''NCHAR(13)'', ''''), + ''CHAR(10)'', ''''), ''CHAR(13)'', '''')' + ELSE N' + dr.deadlock_graph' + END + N' + FROM #deadlock_results AS dr + ORDER BY + dr.event_date, + dr.is_victim DESC + OPTION(RECOMPILE, LOOP JOIN, HASH JOIN); + '; + IF (@OutputDatabaseCheck = 0) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results to table %s', 0, 1, @d) WITH NOWAIT; -/*Making sure top is set to something if NULL*/ -IF ( @Top IS NULL ) - BEGIN - SET @Top = 3; - END; + IF @Debug = 1 + BEGIN + PRINT @deadlock_result; + SET STATISTICS XML ON; + END; -/* -This section determines if you have the Query Store wait stats DMV -*/ + SET @StringToExecute = N' -RAISERROR('Checking for query_store_wait_stats', 0, 1) WITH NOWAIT; + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl + ( + ServerName, + deadlock_type, + event_date, + database_name, + spid, + deadlock_group, + query, + object_names, + isolation_level, + owner_mode, + waiter_mode, + lock_mode, + transaction_count, + client_option_1, + client_option_2, + login_name, + host_name, + client_app, + wait_time, + wait_resource, + priority, + log_used, + last_tran_started, + last_batch_started, + last_batch_completed, + transaction_name, + status, + owner_waiter_type, + owner_activity, + owner_waiter_activity, + owner_merging, + owner_spilling, + owner_waiting_to_close, + waiter_waiter_type, + waiter_owner_activity, + waiter_waiter_activity, + waiter_merging, + waiter_spilling, + waiter_waiting_to_close, + deadlock_graph + ) + EXECUTE sys.sp_executesql + @deadlock_result;'; + EXECUTE sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; -DECLARE @ws_out INT, - @waitstats BIT, - @ws_sql NVARCHAR(MAX) = N'SELECT @i_out = COUNT(*) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.all_objects WHERE name = ''query_store_wait_stats'' OPTION (RECOMPILE);', - @ws_params NVARCHAR(MAX) = N'@i_out INT OUTPUT'; + IF @Debug = 1 + BEGIN + SET STATISTICS XML OFF; + END; -EXEC sys.sp_executesql @ws_sql, @ws_params, @i_out = @ws_out OUTPUT; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; -SELECT @waitstats = CASE @ws_out WHEN 0 THEN 0 ELSE 1 END; + DROP SYNONYM dbo.DeadLockTbl; -SET @msg = N'Wait stats DMV ' + CASE @waitstats - WHEN 0 THEN N' does not exist, skipping.' - WHEN 1 THEN N' exists, will analyze.' - END; -RAISERROR(@msg, 0, 1) WITH NOWAIT; + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; -/* -This section determines if you have some additional columns present in 2017, in case they get back ported. -*/ + SET @StringToExecute = N' -RAISERROR('Checking for new columns in query_store_runtime_stats', 0, 1) WITH NOWAIT; - -DECLARE @nc_out INT, - @new_columns BIT, - @nc_sql NVARCHAR(MAX) = N'SELECT @i_out = COUNT(*) - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.all_columns AS ac - WHERE OBJECT_NAME(object_id) = ''query_store_runtime_stats'' - AND ac.name IN ( - ''avg_num_physical_io_reads'', - ''last_num_physical_io_reads'', - ''min_num_physical_io_reads'', - ''max_num_physical_io_reads'', - ''avg_log_bytes_used'', - ''last_log_bytes_used'', - ''min_log_bytes_used'', - ''max_log_bytes_used'', - ''avg_tempdb_space_used'', - ''last_tempdb_space_used'', - ''min_tempdb_space_used'', - ''max_tempdb_space_used'' - ) OPTION (RECOMPILE);', - @nc_params NVARCHAR(MAX) = N'@i_out INT OUTPUT'; - -EXEC sys.sp_executesql @nc_sql, @ws_params, @i_out = @nc_out OUTPUT; - -SELECT @new_columns = CASE @nc_out WHEN 12 THEN 1 ELSE 0 END; - -SET @msg = N'New query_store_runtime_stats columns ' + CASE @new_columns - WHEN 0 THEN N' do not exist, skipping.' - WHEN 1 THEN N' exist, will analyze.' - END; -RAISERROR(@msg, 0, 1) WITH NOWAIT; + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings + ( + ServerName, + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + @@SERVERNAME, + df.check_id, + df.database_name, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY df.check_id + OPTION(RECOMPILE);'; + EXECUTE sys.sp_executesql @StringToExecute; + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ + END; + ELSE /*Output to database is not set output to client app*/ + 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; + + 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', + ds.proc_name, + sql_handle = + CONVERT(varbinary(64), ds.sql_handle, 1), + dow.database_name, + dow.database_id, + dow.object_name, + query_xml = + TRY_CAST(dr.query_xml AS nvarchar(MAX)) + INTO #available_plans + FROM #deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + JOIN #deadlock_results AS dr + ON dr.id = ds.id + AND dr.event_date = ds.event_date + OPTION(RECOMPILE); - -/* -These are the temp tables we use -*/ + SELECT + deqs.sql_handle, + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset, + deqs.creation_time, + deqs.last_execution_time, + deqs.execution_count, + total_worker_time_ms = + deqs.total_worker_time / 1000., + avg_worker_time_ms = + CONVERT(decimal(38, 6), deqs.total_worker_time / 1000. / deqs.execution_count), + total_elapsed_time_ms = + deqs.total_elapsed_time / 1000., + avg_elapsed_time_ms = + CONVERT(decimal(38, 6), deqs.total_elapsed_time / 1000. / deqs.execution_count), + executions_per_second = + ISNULL + ( + deqs.execution_count / + NULLIF + ( + DATEDIFF + ( + SECOND, + deqs.creation_time, + NULLIF(deqs.last_execution_time, '1900-01-01 00:00:00.000') + ), + 0 + ), + 0 + ), + total_physical_reads_mb = + deqs.total_physical_reads * 8. / 1024., + total_logical_writes_mb = + deqs.total_logical_writes * 8. / 1024., + total_logical_reads_mb = + deqs.total_logical_reads * 8. / 1024., + min_grant_mb = + deqs.min_grant_kb * 8. / 1024., + max_grant_mb = + deqs.max_grant_kb * 8. / 1024., + min_used_grant_mb = + deqs.min_used_grant_kb * 8. / 1024., + max_used_grant_mb = + 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 = + deqs.max_worker_time / 1000., + max_elapsed_time_ms = + deqs.max_elapsed_time / 1000. + INTO #dm_exec_query_stats + FROM sys.dm_exec_query_stats AS deqs + WHERE EXISTS + ( + SELECT + 1/0 + FROM #available_plans AS ap + WHERE ap.sql_handle = deqs.sql_handle + ) + AND deqs.query_hash IS NOT NULL; + + CREATE CLUSTERED INDEX + deqs + ON #dm_exec_query_stats + ( + sql_handle, + plan_handle + ); + + SELECT + ap.available_plans, + ap.database_name, + query_text = + TRY_CAST(ap.query_xml AS xml), + ap.query_plan, + ap.creation_time, + ap.last_execution_time, + ap.execution_count, + ap.executions_per_second, + ap.total_worker_time_ms, + ap.avg_worker_time_ms, + ap.max_worker_time_ms, + ap.total_elapsed_time_ms, + ap.avg_elapsed_time_ms, + ap.max_elapsed_time_ms, + ap.total_logical_reads_mb, + ap.total_physical_reads_mb, + ap.total_logical_writes_mb, + ap.min_grant_mb, + ap.max_grant_mb, + ap.min_used_grant_mb, + ap.max_used_grant_mb, + ap.min_reserved_threads, + ap.max_reserved_threads, + ap.min_used_threads, + ap.max_used_threads, + ap.total_rows, + ap.sql_handle, + ap.statement_start_offset, + ap.statement_end_offset + FROM + ( + + SELECT + ap.*, + c.statement_start_offset, + c.statement_end_offset, + c.creation_time, + c.last_execution_time, + c.execution_count, + c.total_worker_time_ms, + c.avg_worker_time_ms, + c.total_elapsed_time_ms, + c.avg_elapsed_time_ms, + c.executions_per_second, + c.total_physical_reads_mb, + c.total_logical_writes_mb, + c.total_logical_reads_mb, + c.min_grant_mb, + c.max_grant_mb, + c.min_used_grant_mb, + c.max_used_grant_mb, + c.min_reserved_threads, + c.max_reserved_threads, + c.min_used_threads, + c.max_used_threads, + c.total_rows, + c.query_plan, + c.max_worker_time_ms, + c.max_elapsed_time_ms + FROM #available_plans AS ap + OUTER APPLY + ( + SELECT + deqs.*, + query_plan = + TRY_CAST(deps.query_plan AS xml) + FROM #dm_exec_query_stats deqs + OUTER APPLY sys.dm_exec_text_query_plan + ( + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset + ) AS deps + WHERE deqs.sql_handle = ap.sql_handle + AND deps.dbid = ap.database_id + ) AS c + ) AS ap + WHERE ap.query_plan IS NOT NULL + ORDER BY + ap.avg_worker_time_ms DESC + 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, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY + 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.*/ + END; + IF @Debug = 1 + BEGIN + SELECT + table_name = N'#deadlock_data', + * + FROM #deadlock_data AS dd + OPTION(RECOMPILE); -/* -This one holds the grouped data that helps use figure out which periods to examine -*/ + SELECT + table_name = N'#dd', + * + FROM #dd AS d + OPTION(RECOMPILE); -RAISERROR(N'Creating temp tables', 0, 1) WITH NOWAIT; + SELECT + table_name = N'#deadlock_resource', + * + FROM #deadlock_resource AS dr + OPTION(RECOMPILE); -DROP TABLE IF EXISTS #grouped_interval; + SELECT + table_name = N'#deadlock_resource_parallel', + * + FROM #deadlock_resource_parallel AS drp + OPTION(RECOMPILE); -CREATE TABLE #grouped_interval -( - flat_date DATE NULL, - start_range DATETIME NULL, - end_range DATETIME NULL, - total_avg_duration_ms DECIMAL(38, 2) NULL, - total_avg_cpu_time_ms DECIMAL(38, 2) NULL, - total_avg_logical_io_reads_mb DECIMAL(38, 2) NULL, - total_avg_physical_io_reads_mb DECIMAL(38, 2) NULL, - total_avg_logical_io_writes_mb DECIMAL(38, 2) NULL, - total_avg_query_max_used_memory_mb DECIMAL(38, 2) NULL, - total_rowcount DECIMAL(38, 2) NULL, - total_count_executions BIGINT NULL, - total_avg_log_bytes_mb DECIMAL(38, 2) NULL, - total_avg_tempdb_space DECIMAL(38, 2) NULL, - INDEX gi_ix_dates CLUSTERED (start_range, end_range) -); + SELECT + table_name = N'#deadlock_owner_waiter', + * + FROM #deadlock_owner_waiter AS dow + OPTION(RECOMPILE); + SELECT + table_name = N'#deadlock_process', + * + FROM #deadlock_process AS dp + OPTION(RECOMPILE); -/* -These are the plans we focus on based on what we find in the grouped intervals -*/ -DROP TABLE IF EXISTS #working_plans; + SELECT + table_name = N'#deadlock_stack', + * + FROM #deadlock_stack AS ds + OPTION(RECOMPILE); -CREATE TABLE #working_plans -( - plan_id BIGINT, - query_id BIGINT, - pattern NVARCHAR(256), - INDEX wp_ix_ids CLUSTERED (plan_id, query_id) -); + SELECT + table_name = N'#deadlocks', + * + FROM #deadlocks AS d + OPTION(RECOMPILE); + SELECT + table_name = N'#deadlock_results', + * + FROM #deadlock_results AS dr + OPTION(RECOMPILE); -/* -These are the gathered metrics we get from query store to generate some warnings and help you find your worst offenders -*/ -DROP TABLE IF EXISTS #working_metrics; + SELECT + table_name = N'#x', + * + FROM #x AS x + OPTION(RECOMPILE); -CREATE TABLE #working_metrics -( - database_name NVARCHAR(256), - plan_id BIGINT, - query_id BIGINT, - /*these columns are from query_store_query*/ - proc_or_function_name NVARCHAR(256), - batch_sql_handle VARBINARY(64), - query_hash BINARY(8), - query_parameterization_type_desc NVARCHAR(256), - parameter_sniffing_symptoms NVARCHAR(4000), - count_compiles BIGINT, - avg_compile_duration DECIMAL(38,2), - last_compile_duration DECIMAL(38,2), - avg_bind_duration DECIMAL(38,2), - last_bind_duration DECIMAL(38,2), - avg_bind_cpu_time DECIMAL(38,2), - last_bind_cpu_time DECIMAL(38,2), - avg_optimize_duration DECIMAL(38,2), - last_optimize_duration DECIMAL(38,2), - avg_optimize_cpu_time DECIMAL(38,2), - last_optimize_cpu_time DECIMAL(38,2), - avg_compile_memory_kb DECIMAL(38,2), - last_compile_memory_kb DECIMAL(38,2), - /*These come from query_store_runtime_stats*/ - execution_type_desc NVARCHAR(128), - first_execution_time DATETIME2, - last_execution_time DATETIME2, - count_executions BIGINT, - avg_duration DECIMAL(38,2) , - last_duration DECIMAL(38,2), - min_duration DECIMAL(38,2), - max_duration DECIMAL(38,2), - avg_cpu_time DECIMAL(38,2), - last_cpu_time DECIMAL(38,2), - min_cpu_time DECIMAL(38,2), - max_cpu_time DECIMAL(38,2), - avg_logical_io_reads DECIMAL(38,2), - last_logical_io_reads DECIMAL(38,2), - min_logical_io_reads DECIMAL(38,2), - max_logical_io_reads DECIMAL(38,2), - avg_logical_io_writes DECIMAL(38,2), - last_logical_io_writes DECIMAL(38,2), - min_logical_io_writes DECIMAL(38,2), - max_logical_io_writes DECIMAL(38,2), - avg_physical_io_reads DECIMAL(38,2), - last_physical_io_reads DECIMAL(38,2), - min_physical_io_reads DECIMAL(38,2), - max_physical_io_reads DECIMAL(38,2), - avg_clr_time DECIMAL(38,2), - last_clr_time DECIMAL(38,2), - min_clr_time DECIMAL(38,2), - max_clr_time DECIMAL(38,2), - avg_dop BIGINT, - last_dop BIGINT, - min_dop BIGINT, - max_dop BIGINT, - avg_query_max_used_memory DECIMAL(38,2), - last_query_max_used_memory DECIMAL(38,2), - min_query_max_used_memory DECIMAL(38,2), - max_query_max_used_memory DECIMAL(38,2), - avg_rowcount DECIMAL(38,2), - last_rowcount DECIMAL(38,2), - min_rowcount DECIMAL(38,2), - max_rowcount DECIMAL(38,2), - /*These are 2017 only, AFAIK*/ - avg_num_physical_io_reads DECIMAL(38,2), - last_num_physical_io_reads DECIMAL(38,2), - min_num_physical_io_reads DECIMAL(38,2), - max_num_physical_io_reads DECIMAL(38,2), - avg_log_bytes_used DECIMAL(38,2), - last_log_bytes_used DECIMAL(38,2), - min_log_bytes_used DECIMAL(38,2), - max_log_bytes_used DECIMAL(38,2), - avg_tempdb_space_used DECIMAL(38,2), - last_tempdb_space_used DECIMAL(38,2), - min_tempdb_space_used DECIMAL(38,2), - max_tempdb_space_used DECIMAL(38,2), - /*These are computed columns to make some stuff easier down the line*/ - total_compile_duration AS avg_compile_duration * count_compiles, - total_bind_duration AS avg_bind_duration * count_compiles, - total_bind_cpu_time AS avg_bind_cpu_time * count_compiles, - total_optimize_duration AS avg_optimize_duration * count_compiles, - total_optimize_cpu_time AS avg_optimize_cpu_time * count_compiles, - total_compile_memory_kb AS avg_compile_memory_kb * count_compiles, - total_duration AS avg_duration * count_executions, - total_cpu_time AS avg_cpu_time * count_executions, - total_logical_io_reads AS avg_logical_io_reads * count_executions, - total_logical_io_writes AS avg_logical_io_writes * count_executions, - total_physical_io_reads AS avg_physical_io_reads * count_executions, - total_clr_time AS avg_clr_time * count_executions, - total_query_max_used_memory AS avg_query_max_used_memory * count_executions, - total_rowcount AS avg_rowcount * count_executions, - total_num_physical_io_reads AS avg_num_physical_io_reads * count_executions, - total_log_bytes_used AS avg_log_bytes_used * count_executions, - total_tempdb_space_used AS avg_tempdb_space_used * count_executions, - xpm AS NULLIF(count_executions, 0) / NULLIF(DATEDIFF(MINUTE, first_execution_time, last_execution_time), 0), - INDEX wm_ix_ids CLUSTERED (plan_id, query_id, query_hash) -); + SELECT + table_name = N'@sysAssObjId', + * + FROM @sysAssObjId AS s + OPTION(RECOMPILE); + IF OBJECT_ID('tempdb..#available_plans') IS NOT NULL + BEGIN + SELECT + table_name = N'#available_plans', + * + FROM #available_plans AS ap + OPTION(RECOMPILE); + END; -/* -This is where we store some additional metrics, along with the query plan and text -*/ -DROP TABLE IF EXISTS #working_plan_text; + IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL + BEGIN + SELECT + table_name = N'#dm_exec_query_stats', + * + FROM #dm_exec_query_stats + OPTION(RECOMPILE); + END; -CREATE TABLE #working_plan_text -( - database_name NVARCHAR(256), - plan_id BIGINT, - query_id BIGINT, - /*These are from query_store_plan*/ - plan_group_id BIGINT, - engine_version NVARCHAR(64), - compatibility_level INT, - query_plan_hash BINARY(8), - query_plan_xml XML, - is_online_index_plan BIT, - is_trivial_plan BIT, - is_parallel_plan BIT, - is_forced_plan BIT, - is_natively_compiled BIT, - force_failure_count BIGINT, - last_force_failure_reason_desc NVARCHAR(256), - count_compiles BIGINT, - initial_compile_start_time DATETIME2, - last_compile_start_time DATETIME2, - last_execution_time DATETIME2, - avg_compile_duration DECIMAL(38,2), - last_compile_duration BIGINT, - min_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - max_used_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - percent_memory_grant_used AS CONVERT(MONEY, ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100.), - /*These are from query_store_query*/ - query_sql_text NVARCHAR(MAX), - statement_sql_handle VARBINARY(64), - is_part_of_encrypted_module BIT, - has_restricted_text BIT, - /*This is from query_context_settings*/ - context_settings NVARCHAR(512), - /*This is from #working_plans*/ - pattern NVARCHAR(512), - top_three_waits NVARCHAR(MAX), - INDEX wpt_ix_ids CLUSTERED (plan_id, query_id, query_plan_hash) -); + SELECT + procedure_parameters = + 'procedure_parameters', + DatabaseName = + @DatabaseName, + StartDate = + @StartDate, + EndDate = + @EndDate, + ObjectName = + @ObjectName, + StoredProcName = + @StoredProcName, + AppName = + @AppName, + HostName = + @HostName, + LoginName = + @LoginName, + EventSessionName = + @EventSessionName, + TargetSessionType = + @TargetSessionType, + VictimsOnly = + @VictimsOnly, + DeadlockType = + @DeadlockType, + TargetDatabaseName = + @TargetDatabaseName, + TargetSchemaName = + @TargetSchemaName, + TargetTableName = + @TargetTableName, + TargetColumnName = + @TargetColumnName, + TargetTimestampColumnName = + @TargetTimestampColumnName, + Debug = + @Debug, + Help = + @Help, + Version = + @Version, + VersionDate = + @VersionDate, + VersionCheckMode = + @VersionCheckMode, + OutputDatabaseName = + @OutputDatabaseName, + OutputSchemaName = + @OutputSchemaName, + OutputTableName = + @OutputTableName, + ExportToExcel = + @ExportToExcel; + + SELECT + declared_variables = + 'declared_variables', + DatabaseId = + @DatabaseId, + StartDateUTC = + @StartDateUTC, + EndDateUTC = + @EndDateUTC, + ProductVersion = + @ProductVersion, + ProductVersionMajor = + @ProductVersionMajor, + ProductVersionMinor = + @ProductVersionMinor, + ObjectFullName = + @ObjectFullName, + Azure = + @Azure, + RDS = + @RDS, + d = + @d, + StringToExecute = + @StringToExecute, + StringToExecuteParams = + @StringToExecuteParams, + r = + @r, + OutputTableFindings = + @OutputTableFindings, + DeadlockCount = + @DeadlockCount, + ServerName = + @ServerName, + OutputDatabaseCheck = + @OutputDatabaseCheck, + SessionId = + @SessionId, + TargetSessionId = + @TargetSessionId, + FileName = + @FileName, + inputbuf_bom = + @inputbuf_bom, + deadlock_result = + @deadlock_result; + END; /*End debug*/ + END; /*Final End*/ +GO +IF OBJECT_ID('dbo.sp_BlitzWho') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzWho AS RETURN 0;') +GO +ALTER PROCEDURE dbo.sp_BlitzWho + @Help TINYINT = 0 , + @ShowSleepingSPIDs TINYINT = 0, + @ExpertMode BIT = 0, + @Debug BIT = 0, + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputTableRetentionDays TINYINT = 3 , + @MinElapsedSeconds INT = 0 , + @MinCPUTime INT = 0 , + @MinLogicalReads INT = 0 , + @MinPhysicalReads INT = 0 , + @MinWrites INT = 0 , + @MinTempdbMB INT = 0 , + @MinRequestedMemoryKB INT = 0 , + @MinBlockingSeconds INT = 0 , + @CheckDateOverride DATETIMEOFFSET = NULL, + @ShowActualParameters BIT = 0, + @GetOuterCommand BIT = 0, + @GetLiveQueryPlan BIT = NULL, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @SortOrder NVARCHAR(256) = N'elapsed time' +AS +BEGIN + SET NOCOUNT ON; + SET STATISTICS XML OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; -/* -This is where we store warnings that we generate from the XML and metrics -*/ -DROP TABLE IF EXISTS #working_warnings; -CREATE TABLE #working_warnings -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_or_function_name NVARCHAR(256), - plan_multiple_plans BIT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - query_cost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - is_trivial BIT, - trace_flags_session NVARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name NVARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - is_slow_plan BIT, - is_compile_more BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_big_log BIT, - is_big_tempdb BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - warnings NVARCHAR(4000) - INDEX ww_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); + IF @Help = 1 + BEGIN + PRINT ' +sp_BlitzWho from http://FirstResponderKit.org -DROP TABLE IF EXISTS #working_wait_stats; +This script gives you a snapshot of everything currently executing on your SQL Server. -CREATE TABLE #working_wait_stats -( - plan_id BIGINT, - wait_category TINYINT, - wait_category_desc NVARCHAR(256), - total_query_wait_time_ms BIGINT, - avg_query_wait_time_ms DECIMAL(38, 2), - last_query_wait_time_ms BIGINT, - min_query_wait_time_ms BIGINT, - max_query_wait_time_ms BIGINT, - wait_category_mapped AS CASE wait_category - WHEN 0 THEN N'UNKNOWN' - WHEN 1 THEN N'SOS_SCHEDULER_YIELD' - WHEN 2 THEN N'THREADPOOL' - WHEN 3 THEN N'LCK_M_%' - WHEN 4 THEN N'LATCH_%' - WHEN 5 THEN N'PAGELATCH_%' - WHEN 6 THEN N'PAGEIOLATCH_%' - WHEN 7 THEN N'RESOURCE_SEMAPHORE_QUERY_COMPILE' - WHEN 8 THEN N'CLR%, SQLCLR%' - WHEN 9 THEN N'DBMIRROR%' - WHEN 10 THEN N'XACT%, DTC%, TRAN_MARKLATCH_%, MSQL_XACT_%, TRANSACTION_MUTEX' - WHEN 11 THEN N'SLEEP_%, LAZYWRITER_SLEEP, SQLTRACE_BUFFER_FLUSH, SQLTRACE_INCREMENTAL_FLUSH_SLEEP, SQLTRACE_WAIT_ENTRIES, FT_IFTS_SCHEDULER_IDLE_WAIT, XE_DISPATCHER_WAIT, REQUEST_FOR_DEADLOCK_SEARCH, LOGMGR_QUEUE, ONDEMAND_TASK_QUEUE, CHECKPOINT_QUEUE, XE_TIMER_EVENT' - WHEN 12 THEN N'PREEMPTIVE_%' - WHEN 13 THEN N'BROKER_% (but not BROKER_RECEIVE_WAITFOR)' - WHEN 14 THEN N'LOGMGR, LOGBUFFER, LOGMGR_RESERVE_APPEND, LOGMGR_FLUSH, LOGMGR_PMM_LOG, CHKPT, WRITELOG' - WHEN 15 THEN N'ASYNC_NETWORK_IO, NET_WAITFOR_PACKET, PROXY_NETWORK_IO, EXTERNAL_SCRIPT_NETWORK_IOF' - WHEN 16 THEN N'CXPACKET, EXCHANGE' - WHEN 17 THEN N'RESOURCE_SEMAPHORE, CMEMTHREAD, CMEMPARTITIONED, EE_PMOLOCK, MEMORY_ALLOCATION_EXT, RESERVED_MEMORY_ALLOCATION_EXT, MEMORY_GRANT_UPDATE' - WHEN 18 THEN N'WAITFOR, WAIT_FOR_RESULTS, BROKER_RECEIVE_WAITFOR' - WHEN 19 THEN N'TRACEWRITE, SQLTRACE_LOCK, SQLTRACE_FILE_BUFFER, SQLTRACE_FILE_WRITE_IO_COMPLETION, SQLTRACE_FILE_READ_IO_COMPLETION, SQLTRACE_PENDING_BUFFER_WRITERS, SQLTRACE_SHUTDOWN, QUERY_TRACEOUT, TRACE_EVTNOTIFF' - WHEN 20 THEN N'FT_RESTART_CRAWL, FULLTEXT GATHERER, MSSEARCH, FT_METADATA_MUTEX, FT_IFTSHC_MUTEX, FT_IFTSISM_MUTEX, FT_IFTS_RWLOCK, FT_COMPROWSET_RWLOCK, FT_MASTER_MERGE, FT_PROPERTYLIST_CACHE, FT_MASTER_MERGE_COORDINATOR, PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC' - WHEN 21 THEN N'ASYNC_IO_COMPLETION, IO_COMPLETION, BACKUPIO, WRITE_COMPLETION, IO_QUEUE_LIMIT, IO_RETRY' - WHEN 22 THEN N'SE_REPL_%, REPL_%, HADR_% (but not HADR_THROTTLE_LOG_RATE_GOVERNOR), PWAIT_HADR_%, REPLICA_WRITES, FCB_REPLICA_WRITE, FCB_REPLICA_READ, PWAIT_HADRSIM' - WHEN 23 THEN N'LOG_RATE_GOVERNOR, POOL_LOG_RATE_GOVERNOR, HADR_THROTTLE_LOG_RATE_GOVERNOR, INSTANCE_LOG_RATE_GOVERNOR' - END, - INDEX wws_ix_ids CLUSTERED ( plan_id) -); +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. +Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Outputting to table is only supported with SQL Server 2012 and higher. + - If @OutputDatabaseName and @OutputSchemaName are populated, the database and + schema must already exist. We will not create them, only the table. + +MIT License -/* -The next three tables hold plan XML parsed out to different degrees -*/ -DROP TABLE IF EXISTS #statements; +Copyright (c) Brent Ozar Unlimited -CREATE TABLE #statements -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - statement XML, - INDEX s_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); +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. -DROP TABLE IF EXISTS #query_plan; +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. +'; +RETURN; +END; /* @Help = 1 */ -CREATE TABLE #query_plan -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - query_plan XML, - INDEX qp_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); +/* Get the major and minor build numbers */ +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]) + ,@EnhanceFlag BIT = 0 + ,@BlockingCheck NVARCHAR(MAX) + ,@StringToSelect NVARCHAR(MAX) + ,@StringToExecute NVARCHAR(MAX) + ,@OutputTableCleanupDate DATE + ,@SessionWaits BIT = 0 + ,@SessionWaitsSQL NVARCHAR(MAX) = + N'LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT TOP 5 waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_time_ms) AS NVARCHAR(128)) + + N'' ms), '' + FROM sys.dm_exec_session_wait_stats AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + HAVING SUM(waitwait.wait_time_ms) > 5 + ORDER BY 1 + FOR + XML PATH('''') ) AS session_wait_info + FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 + ON s.session_id = wt2.session_id + LEFT JOIN sys.dm_exec_query_stats AS session_stats + ON r.sql_handle = session_stats.sql_handle + AND r.plan_handle = session_stats.plan_handle + AND r.statement_start_offset = session_stats.statement_start_offset + AND r.statement_end_offset = session_stats.statement_end_offset' + ,@ObjectFullName NVARCHAR(2000) + ,@OutputTableNameQueryStats_View NVARCHAR(256) + ,@LineFeed NVARCHAR(MAX) /* Had to set as MAX up from 10 as it was truncating the view creation*/; +/* Let's get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); -DROP TABLE IF EXISTS #relop; +SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), + @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) -CREATE TABLE #relop -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - relop XML, - INDEX ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); +SELECT + @OutputTableNameQueryStats_View = QUOTENAME(@OutputTableName + '_Deltas'), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @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 -DROP TABLE IF EXISTS #plan_cost; +IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ExpertMode = 1; /* Force ExpertMode when we're logging to table */ + + /* Create the table if it doesn't exist */ + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'('; + SET @StringToExecute = @StringToExecute + N' + ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128) NOT NULL, + CheckDate DATETIMEOFFSET NOT NULL, + [elapsed_time] [varchar](41) NULL, + [session_id] [smallint] NOT NULL, + [database_name] [nvarchar](128) NULL, + [query_text] [nvarchar](max) NULL, + [outer_command] NVARCHAR(4000) NULL, + [query_plan] [xml] NULL, + [live_query_plan] [xml] NULL, + [cached_parameter_info] [nvarchar](max) NULL, + [live_parameter_info] [nvarchar](max) NULL, + [query_cost] [float] NULL, + [status] [nvarchar](30) NOT NULL, + [wait_info] [nvarchar](max) NULL, + [wait_resource] [nvarchar](max) NULL, + [top_session_waits] [nvarchar](max) NULL, + [blocking_session_id] [smallint] NULL, + [open_transaction_count] [int] NULL, + [is_implicit_transaction] [int] NOT NULL, + [nt_domain] [nvarchar](128) NULL, + [host_name] [nvarchar](128) NULL, + [login_name] [nvarchar](128) NOT NULL, + [nt_user_name] [nvarchar](128) NULL, + [program_name] [nvarchar](128) NULL, + [fix_parameter_sniffing] [nvarchar](150) NULL, + [client_interface_name] [nvarchar](32) NULL, + [login_time] [datetime] NOT NULL, + [start_time] [datetime] NULL, + [request_time] [datetime] NULL, + [request_cpu_time] [int] NULL, + [request_logical_reads] [bigint] NULL, + [request_writes] [bigint] NULL, + [request_physical_reads] [bigint] NULL, + [session_cpu] [int] NOT NULL, + [session_logical_reads] [bigint] NOT NULL, + [session_physical_reads] [bigint] NOT NULL, + [session_writes] [bigint] NOT NULL, + [tempdb_allocations_mb] [decimal](38, 2) NULL, + [memory_usage] [int] NOT NULL, + [estimated_completion_time] [bigint] NULL, + [percent_complete] [real] NULL, + [deadlock_priority] [int] NULL, + [transaction_isolation_level] [varchar](33) NOT NULL, + [degree_of_parallelism] [smallint] NULL, + [last_dop] [bigint] NULL, + [min_dop] [bigint] NULL, + [max_dop] [bigint] NULL, + [last_grant_kb] [bigint] NULL, + [min_grant_kb] [bigint] NULL, + [max_grant_kb] [bigint] NULL, + [last_used_grant_kb] [bigint] NULL, + [min_used_grant_kb] [bigint] NULL, + [max_used_grant_kb] [bigint] NULL, + [last_ideal_grant_kb] [bigint] NULL, + [min_ideal_grant_kb] [bigint] NULL, + [max_ideal_grant_kb] [bigint] NULL, + [last_reserved_threads] [bigint] NULL, + [min_reserved_threads] [bigint] NULL, + [max_reserved_threads] [bigint] NULL, + [last_used_threads] [bigint] NULL, + [min_used_threads] [bigint] NULL, + [max_used_threads] [bigint] NULL, + [grant_time] [varchar](20) NULL, + [requested_memory_kb] [bigint] NULL, + [grant_memory_kb] [bigint] NULL, + [is_request_granted] [varchar](39) NOT NULL, + [required_memory_kb] [bigint] NULL, + [query_memory_grant_used_memory_kb] [bigint] NULL, + [ideal_memory_kb] [bigint] NULL, + [is_small] [bit] NULL, + [timeout_sec] [int] NULL, + [resource_semaphore_id] [smallint] NULL, + [wait_order] [varchar](20) NULL, + [wait_time_ms] [varchar](20) NULL, + [next_candidate_for_memory_grant] [varchar](3) NOT NULL, + [target_memory_kb] [bigint] NULL, + [max_target_memory_kb] [varchar](30) NULL, + [total_memory_kb] [bigint] NULL, + [available_memory_kb] [bigint] NULL, + [granted_memory_kb] [bigint] NULL, + [query_resource_semaphore_used_memory_kb] [bigint] NULL, + [grantee_count] [int] NULL, + [waiter_count] [int] NULL, + [timeout_error_count] [bigint] NULL, + [forced_grant_count] [varchar](30) NULL, + [workload_group_name] [sysname] NULL, + [resource_pool_name] [sysname] NULL, + [context_info] [varchar](128) NULL, + [query_hash] [binary](8) NULL, + [query_plan_hash] [binary](8) NULL, + [sql_handle] [varbinary] (64) NULL, + [plan_handle] [varbinary] (64) NULL, + [statement_start_offset] INT NULL, + [statement_end_offset] INT NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC(@StringToExecute); + + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new cached_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''cached_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD cached_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new live_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''live_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD live_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new outer_command column, add it. See Github #2887. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''outer_command'') + ALTER TABLE ' + @ObjectFullName + N' ADD outer_command NVARCHAR(4000) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new wait_resource column, add it. See Github #2970. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''wait_resource'') + ALTER TABLE ' + @ObjectFullName + N' ADD wait_resource NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* Delete history older than @OutputTableRetentionDays */ + SET @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + N''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + N' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @@SERVERNAME, @OutputTableCleanupDate; -CREATE TABLE #plan_cost -( - query_plan_cost DECIMAL(38,2), - sql_handle VARBINARY(64), - plan_id INT, - INDEX px_ix_ids CLUSTERED (sql_handle, plan_id) -); + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameQueryStats_View; + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameQueryStats_View + N' AS ' + @LineFeed + + N'WITH MaxQueryDuration AS ' + @LineFeed + + N'( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' MIN([ID]) AS [MinID], ' + @LineFeed + + N' MAX([ID]) AS [MaxID] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' GROUP BY [ServerName], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [sql_handle] ' + @LineFeed + + N') ' + @LineFeed + + N'SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @LineFeed + + N' ( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' /* Truncate the query text to aid performance of painting the rows in SSMS */ ' + @LineFeed + + N' CAST([query_text] AS NVARCHAR(1000)) AS [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' ((CAST([request_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' ((CAST([request_writes] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' ((CAST([request_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' ((CAST([session_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' ((CAST([session_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' ((CAST([session_writes] AS DECIMAL(38,2))* 8)/ 1024) [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' ) AS [BlitzWho] ' + @LineFeed + + N'INNER JOIN [MaxQueryDuration] ON [BlitzWho].[ID] = [MaxQueryDuration].[MaxID]; ' + @LineFeed + + N''');' -DROP TABLE IF EXISTS #est_rows; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END -CREATE TABLE #est_rows -( - estimated_rows DECIMAL(38,2), - query_hash BINARY(8), - INDEX px_ix_ids CLUSTERED (query_hash) -); + EXEC(@StringToExecute); + END; + END -DROP TABLE IF EXISTS #stats_agg; + IF OBJECT_ID('tempdb..#WhoReadableDBs') IS NOT NULL + DROP TABLE #WhoReadableDBs; -CREATE TABLE #stats_agg +CREATE TABLE #WhoReadableDBs ( - sql_handle VARBINARY(64), - last_update DATETIME2, - modification_count DECIMAL(38, 2), - sampling_percent DECIMAL(38, 2), - [statistics] NVARCHAR(256), - [table] NVARCHAR(256), - [schema] NVARCHAR(256), - [database] NVARCHAR(256), - INDEX sa_ix_ids CLUSTERED (sql_handle) +database_id INT ); +IF 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; -DROP TABLE IF EXISTS #trace_flags; - -CREATE TABLE #trace_flags -( - sql_handle VARBINARY(54), - global_trace_flags NVARCHAR(4000), - session_trace_flags NVARCHAR(4000), - INDEX tf_ix_ids CLUSTERED (sql_handle) -); - + EXEC('INSERT INTO #WhoReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'); +END -DROP TABLE IF EXISTS #warning_results; +SELECT @BlockingCheck = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SET LOCK_TIMEOUT 1000; /* To avoid blocking on live query plans. See Github issue #2907. */ + DECLARE @blocked TABLE + ( + dbid SMALLINT NOT NULL, + last_batch DATETIME NOT NULL, + open_tran SMALLINT NOT NULL, + sql_handle BINARY(20) NOT NULL, + session_id SMALLINT NOT NULL, + blocking_session_id SMALLINT NOT NULL, + lastwaittype NCHAR(32) NOT NULL, + waittime BIGINT NOT NULL, + cpu INT NOT NULL, + physical_io BIGINT NOT NULL, + memusage INT NOT NULL + ); + + INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) + SELECT + sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, + sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage + FROM sys.sysprocesses AS sys1 + JOIN sys.sysprocesses AS sys2 + ON sys1.spid = sys2.blocked; + '+CASE + WHEN (@GetOuterCommand = 1 AND (NOT EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer'))) THEN N' + DECLARE @session_id SMALLINT; + DECLARE @Sessions TABLE + ( + session_id INT + ); -CREATE TABLE #warning_results -( - ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED, - CheckID INT, - Priority TINYINT, - FindingsGroup NVARCHAR(50), - Finding NVARCHAR(200), - URL NVARCHAR(200), - Details NVARCHAR(4000) -); + DECLARE @inputbuffer TABLE + ( + ID INT IDENTITY(1,1), + session_id INT, + event_type NVARCHAR(30), + parameters SMALLINT, + event_info NVARCHAR(4000) + ); -/*These next three tables hold information about implicit conversion and cached parameters */ -DROP TABLE IF EXISTS #stored_proc_info; + DECLARE inputbuffer_cursor + + CURSOR LOCAL FAST_FORWARD + FOR + SELECT session_id + FROM sys.dm_exec_sessions + WHERE session_id <> @@SPID + AND is_user_process = 1; + + OPEN inputbuffer_cursor; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id; + + WHILE (@@FETCH_STATUS = 0) + BEGIN; + BEGIN TRY; + + INSERT INTO @inputbuffer ([event_type],[parameters],[event_info]) + EXEC sp_executesql + N''DBCC INPUTBUFFER(@session_id) WITH NO_INFOMSGS;'', + N''@session_id SMALLINT'', + @session_id; + + UPDATE @inputbuffer + SET session_id = @session_id + WHERE ID = SCOPE_IDENTITY(); + + END TRY + BEGIN CATCH + RAISERROR(''DBCC inputbuffer failed for session %d'',0,0,@session_id) WITH NOWAIT; + END CATCH; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id + + END; + + CLOSE inputbuffer_cursor; + DEALLOCATE inputbuffer_cursor;' + ELSE N'' + END+ + N' + + DECLARE @LiveQueryPlans TABLE + ( + Session_Id INT NOT NULL, + Query_Plan XML NOT NULL + ); -CREATE TABLE #stored_proc_info -( - sql_handle VARBINARY(64), - query_hash BINARY(8), - variable_name NVARCHAR(128), - variable_datatype NVARCHAR(128), - converted_column_name NVARCHAR(128), - compile_time_value NVARCHAR(128), - proc_name NVARCHAR(300), - column_name NVARCHAR(128), - converted_to NVARCHAR(128) - INDEX tf_ix_ids CLUSTERED (sql_handle, query_hash) -); + ' +IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan' AND @GetLiveQueryPlan=1) +BEGIN + SET @BlockingCheck = @BlockingCheck + N' + INSERT INTO @LiveQueryPlans + SELECT s.session_id, query_plan + FROM sys.dm_exec_sessions AS s + CROSS APPLY sys.dm_exec_query_statistics_xml(s.session_id) + WHERE s.session_id <> @@SPID;'; +END -DROP TABLE IF EXISTS #variable_info -CREATE TABLE #variable_info -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_name NVARCHAR(128), - variable_name NVARCHAR(200), - variable_datatype NVARCHAR(128), - compile_time_value NVARCHAR(4000), - INDEX vif_ix_ids CLUSTERED (sql_handle, query_hash) -); +IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 +BEGIN + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SET @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id + ELSE NULL + END AS blocking_session_id, + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource , + COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END + + IF @ExpertMode = 1 + BEGIN + SET @StringToExecute += + N', + ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , + COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, + tempdb_allocations.tempdb_allocations_mb, + s.memory_usage , + r.estimated_completion_time , + r.percent_complete , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , + qmg.dop AS degree_of_parallelism , + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, + wg.name AS workload_group_name , + rp.name AS resource_pool_name, + CONVERT(VARCHAR(128), r.context_info) AS context_info + ' + END /* IF @ExpertMode = 1 */ + + SET @StringToExecute += + N'FROM sys.dm_exec_sessions AS s + '+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N'OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N'LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle + AND r.plan_handle = query_stats.plan_handle + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id + AND r.request_id = qmg.request_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.resource_governor_workload_groups wg + ON s.group_id = wg.group_id + LEFT JOIN sys.resource_governor_resource_pools rp + ON wg.pool_id = rp.pool_id + OUTER APPLY ( + SELECT TOP 1 + b.dbid, b.last_batch, b.open_tran, b.sql_handle, + b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime + FROM @blocked b + WHERE (s.session_id = b.session_id + OR s.session_id = b.blocking_session_id) + ) AS blocked + OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY ( + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb + FROM sys.dm_db_task_space_usage tsu + WHERE tsu.request_id = r.request_id + AND tsu.session_id = r.session_id + AND tsu.session_id = s.session_id + ) as tempdb_allocations + WHERE s.session_id <> @@SPID + AND s.host_name IS NOT NULL + ' + + CASE WHEN @ShowSleepingSPIDs = 0 THEN + N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' + WHEN @ShowSleepingSPIDs = 1 THEN + N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' + ELSE N'' END; +END /* IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 */ -DROP TABLE IF EXISTS #conversion_info +IF @ProductVersionMajor >= 11 + BEGIN + SELECT @EnhanceFlag = + CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 + WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 + WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 + WHEN @ProductVersionMajor > 13 THEN 1 + ELSE 0 + END -CREATE TABLE #conversion_info -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_name NVARCHAR(128), - expression NVARCHAR(4000), - at_charindex AS CHARINDEX('@', expression), - bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), - comma_charindex AS CHARINDEX(',', expression) + 1, - second_comma_charindex AS - CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, - equal_charindex AS CHARINDEX('=', expression) + 1, - paren_charindex AS CHARINDEX('(', expression) + 1, - comma_paren_charindex AS - CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, - convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression), - INDEX cif_ix_ids CLUSTERED (sql_handle, query_hash) -); -/* These tables support the Missing Index details clickable*/ + IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL + BEGIN + SET @SessionWaits = 1 + END + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SELECT @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id + ELSE NULL + END AS blocking_session_id, + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 + THEN '''''' + ELSE '''''' + END + +') AS XML + + + ) AS live_query_plan , + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + FROM derp.query_plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Cached_Parameter_Info, + ' + IF @ShowActualParameters = 1 + BEGIN + SELECT @StringToExecute = @StringToExecute + N'qs_live.Live_Parameter_Info as Live_Parameter_Info,' + END -DROP TABLE IF EXISTS #missing_index_xml + SELECT @StringToExecute = @StringToExecute + N' + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource ,' + + + CASE @SessionWaits + WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' + ELSE N' NULL AS top_session_waits ,' + END + + + N'COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END -CREATE TABLE #missing_index_xml -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - index_xml XML -); + IF @ExpertMode = 1 /* We show more columns in expert mode, so the SELECT gets longer */ + BEGIN + SET @StringToExecute += + N', ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , + COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, + tempdb_allocations.tempdb_allocations_mb, + s.memory_usage , + r.estimated_completion_time , + r.percent_complete , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , + qmg.dop AS degree_of_parallelism , ' + + + CASE @EnhanceFlag + WHEN 1 THEN N'query_stats.last_dop, + query_stats.min_dop, + query_stats.max_dop, + query_stats.last_grant_kb, + query_stats.min_grant_kb, + query_stats.max_grant_kb, + query_stats.last_used_grant_kb, + query_stats.min_used_grant_kb, + query_stats.max_used_grant_kb, + query_stats.last_ideal_grant_kb, + query_stats.min_ideal_grant_kb, + query_stats.max_ideal_grant_kb, + query_stats.last_reserved_threads, + query_stats.min_reserved_threads, + query_stats.max_reserved_threads, + query_stats.last_used_threads, + query_stats.min_used_threads, + query_stats.max_used_threads,' + ELSE N' NULL AS last_dop, + NULL AS min_dop, + NULL AS max_dop, + NULL AS last_grant_kb, + NULL AS min_grant_kb, + NULL AS max_grant_kb, + NULL AS last_used_grant_kb, + NULL AS min_used_grant_kb, + NULL AS max_used_grant_kb, + NULL AS last_ideal_grant_kb, + NULL AS min_ideal_grant_kb, + NULL AS max_ideal_grant_kb, + NULL AS last_reserved_threads, + NULL AS min_reserved_threads, + NULL AS max_reserved_threads, + NULL AS last_used_threads, + NULL AS min_used_threads, + NULL AS max_used_threads,' + END + + SET @StringToExecute += + N' + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, + wg.name AS workload_group_name, + rp.name AS resource_pool_name, + CONVERT(VARCHAR(128), r.context_info) AS context_info, + r.query_hash, r.query_plan_hash, r.sql_handle, r.plan_handle, r.statement_start_offset, r.statement_end_offset ' + END /* IF @ExpertMode = 1 */ + + SET @StringToExecute += + N' FROM sys.dm_exec_sessions AS s'+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N' + OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N' + LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle + AND r.plan_handle = query_stats.plan_handle + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + ' + + + CASE @SessionWaits + WHEN 1 THEN @SessionWaitsSQL + ELSE N'' + END + + + N' + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id + AND r.request_id = qmg.request_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.resource_governor_workload_groups wg + ON s.group_id = wg.group_id + LEFT JOIN sys.resource_governor_resource_pools rp + ON wg.pool_id = rp.pool_id + OUTER APPLY ( + SELECT TOP 1 + b.dbid, b.last_batch, b.open_tran, b.sql_handle, + b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime + FROM @blocked b + WHERE (s.session_id = b.session_id + OR s.session_id = b.blocking_session_id) + ) AS blocked + OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY ( + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb + FROM sys.dm_db_task_space_usage tsu + WHERE tsu.request_id = r.request_id + AND tsu.session_id = r.session_id + AND tsu.session_id = s.session_id + ) as tempdb_allocations + + OUTER APPLY ( + SELECT TOP 1 Query_Plan, + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + N'' (Actual: '' + Node.Data.value(''(@ParameterRuntimeValue)[1]'', ''NVARCHAR(4000)'') + N'')'' + FROM q.Query_Plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Live_Parameter_Info + FROM @LiveQueryPlans q + WHERE (s.session_id = q.Session_Id) + + ) AS qs_live + + WHERE s.session_id <> @@SPID + AND s.host_name IS NOT NULL + AND r.database_id NOT IN (SELECT database_id FROM #WhoReadableDBs) + ' + + CASE WHEN @ShowSleepingSPIDs = 0 THEN + N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' + WHEN @ShowSleepingSPIDs = 1 THEN + N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' + ELSE N'' END; + + +END /* IF @ProductVersionMajor >= 11 */ + +IF (@MinElapsedSeconds + @MinCPUTime + @MinLogicalReads + @MinPhysicalReads + @MinWrites + @MinTempdbMB + @MinRequestedMemoryKB + @MinBlockingSeconds) > 0 + BEGIN + /* They're filtering for something, so set up a where clause that will let any (not all combined) of the min triggers work: */ + SET @StringToExecute += N' AND (1 = 0 '; + IF @MinElapsedSeconds > 0 + SET @StringToExecute += N' OR ABS(COALESCE(r.total_elapsed_time,0)) / 1000 >= ' + CAST(@MinElapsedSeconds AS NVARCHAR(20)); + IF @MinCPUTime > 0 + SET @StringToExecute += N' OR COALESCE(r.cpu_time, s.cpu_time,0) / 1000 >= ' + CAST(@MinCPUTime AS NVARCHAR(20)); + IF @MinLogicalReads > 0 + SET @StringToExecute += N' OR COALESCE(r.logical_reads, s.logical_reads,0) >= ' + CAST(@MinLogicalReads AS NVARCHAR(20)); + IF @MinPhysicalReads > 0 + SET @StringToExecute += N' OR COALESCE(s.reads,0) >= ' + CAST(@MinPhysicalReads AS NVARCHAR(20)); + IF @MinWrites > 0 + SET @StringToExecute += N' OR COALESCE(r.writes, s.writes,0) >= ' + CAST(@MinWrites AS NVARCHAR(20)); + IF @MinTempdbMB > 0 + SET @StringToExecute += N' OR COALESCE(tempdb_allocations.tempdb_allocations_mb,0) >= ' + CAST(@MinTempdbMB AS NVARCHAR(20)); + IF @MinRequestedMemoryKB > 0 + SET @StringToExecute += N' OR COALESCE(qmg.requested_memory_kb,0) >= ' + CAST(@MinRequestedMemoryKB AS NVARCHAR(20)); + /* Blocking is a little different - we're going to return ALL of the queries if we meet the blocking threshold. */ + IF @MinBlockingSeconds > 0 + SET @StringToExecute += N' OR (SELECT SUM(waittime / 1000) FROM @blocked) >= ' + CAST(@MinBlockingSeconds AS NVARCHAR(20)); + SET @StringToExecute += N' ) '; + END -DROP TABLE IF EXISTS #missing_index_schema +SET @StringToExecute += + N' ORDER BY ' + CASE WHEN @SortOrder = 'session_id' THEN '[session_id] DESC' + WHEN @SortOrder = 'query_cost' THEN '[query_cost] DESC' + WHEN @SortOrder = 'database_name' THEN '[database_name] ASC' + WHEN @SortOrder = 'open_transaction_count' THEN '[open_transaction_count] DESC' + WHEN @SortOrder = 'is_implicit_transaction' THEN '[is_implicit_transaction] DESC' + WHEN @SortOrder = 'login_name' THEN '[login_name] ASC' + WHEN @SortOrder = 'program_name' THEN '[program_name] ASC' + WHEN @SortOrder = 'client_interface_name' THEN '[client_interface_name] ASC' + WHEN @SortOrder = 'request_cpu_time' THEN 'COALESCE(r.cpu_time, s.cpu_time) DESC' + WHEN @SortOrder = 'request_logical_reads' THEN 'COALESCE(r.logical_reads, s.logical_reads) DESC' + WHEN @SortOrder = 'request_writes' THEN 'COALESCE(r.writes, s.writes) DESC' + WHEN @SortOrder = 'request_physical_reads' THEN 'COALESCE(r.reads, s.reads) DESC ' + WHEN @SortOrder = 'session_cpu' THEN 's.cpu_time DESC' + WHEN @SortOrder = 'session_logical_reads' THEN 's.logical_reads DESC' + WHEN @SortOrder = 'session_physical_reads' THEN 's.reads DESC' + WHEN @SortOrder = 'session_writes' THEN 's.writes DESC' + WHEN @SortOrder = 'tempdb_allocations_mb' THEN '[tempdb_allocations_mb] DESC' + WHEN @SortOrder = 'memory_usage' THEN '[memory_usage] DESC' + WHEN @SortOrder = 'deadlock_priority' THEN 'r.deadlock_priority DESC' + WHEN @SortOrder = 'transaction_isolation_level' THEN 'r.[transaction_isolation_level] DESC' + WHEN @SortOrder = 'requested_memory_kb' THEN '[requested_memory_kb] DESC' + WHEN @SortOrder = 'grant_memory_kb' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'grant' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'query_memory_grant_used_memory_kb' THEN 'qmg.used_memory_kb DESC' + WHEN @SortOrder = 'ideal_memory_kb' THEN '[ideal_memory_kb] DESC' + WHEN @SortOrder = 'workload_group_name' THEN 'wg.name ASC' + WHEN @SortOrder = 'resource_pool_name' THEN 'rp.name ASC' + ELSE '[elapsed_time] DESC' + END + ' + '; -CREATE TABLE #missing_index_schema -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - index_xml XML -); +IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + N'; ' + + @BlockingCheck + + + ' INSERT INTO ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'(ServerName + ,CheckDate + ,[elapsed_time] + ,[session_id] + ,[blocking_session_id] + ,[database_name] + ,[query_text]' + + CASE WHEN @GetOuterCommand = 1 THEN N',[outer_command]' ELSE N'' END + N' + ,[query_plan]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[live_query_plan]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[cached_parameter_info]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 AND @ShowActualParameters = 1 THEN N',[Live_Parameter_Info]' ELSE N'' END + N' + ,[query_cost] + ,[status] + ,[wait_info] + ,[wait_resource]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[top_session_waits]' ELSE N'' END + N' + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[request_logical_reads] + ,[request_writes] + ,[request_physical_reads] + ,[session_cpu] + ,[session_logical_reads] + ,[session_physical_reads] + ,[session_writes] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[degree_of_parallelism]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads]' ELSE N'' END + N' + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset]' ELSE N'' END + N' +) + SELECT @@SERVERNAME, COALESCE(@CheckDateOverride, SYSDATETIMEOFFSET()) AS CheckDate , ' + + @StringToExecute; + END +ELSE + SET @StringToExecute = @BlockingCheck + N' SELECT GETDATE() AS run_date , ' + @StringToExecute; + +/* If the server has > 50GB of memory, add a max grant hint to avoid getting a giant grant */ +IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020) + OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 ) + OR (@ProductVersionMajor >= 13 ) + AND 50000000 < (SELECT cntr_value + FROM sys.dm_os_performance_counters + WHERE object_name LIKE '%:Memory Manager%' + AND counter_name LIKE 'Target Server Memory (KB)%') + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (MAX_GRANT_PERCENT = 1, RECOMPILE) '; + END +ELSE + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (RECOMPILE) '; + END -DROP TABLE IF EXISTS #missing_index_usage +/* Be good: */ +SET @StringToExecute = @StringToExecute + N' ; '; -CREATE TABLE #missing_index_usage -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - index_xml XML -); -DROP TABLE IF EXISTS #missing_index_detail +IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END -CREATE TABLE #missing_index_detail -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - column_name NVARCHAR(128) -); +EXEC sp_executesql @StringToExecute, + N'@CheckDateOverride DATETIMEOFFSET', + @CheckDateOverride; +END +GO +IF OBJECT_ID('dbo.CommandExecute') IS NULL +BEGIN + PRINT 'sp_DatabaseRestore is about to install, but you have not yet installed the Ola Hallengren maintenance scripts.' + PRINT 'sp_DatabaseRestore will still install, but to use that stored proc, you will need to install this:' + PRINT 'https://ola.hallengren.com' + PRINT 'You will see a bunch of warnings below because the Ola scripts are not installed yet, and that is okay:' +END +GO -DROP TABLE IF EXISTS #missing_index_pretty -CREATE TABLE #missing_index_pretty -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), - details AS N'/* ' - + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' - + CONVERT(NVARCHAR(30), impact) - + '%.' - + CHAR(10) - + N'*/' - + CHAR(10) + CHAR(13) - + N'/* ' - + CHAR(10) - + N'USE ' - + database_name - + CHAR(10) - + N'GO' - + CHAR(10) + CHAR(13) - + N'CREATE NONCLUSTERED INDEX ix_' - + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') - + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END - + CHAR(10) - + N' ON ' - + schema_name - + N'.' - + table_name - + N' (' + - + CASE WHEN equality IS NOT NULL - THEN equality - + CASE WHEN inequality IS NOT NULL - THEN N', ' + inequality - ELSE N'' - END - ELSE inequality - END - + N')' - + CHAR(10) - + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' - END - + CHAR(10) - + N'GO' - + CHAR(10) - + N'*/' -); +IF OBJECT_ID('dbo.sp_DatabaseRestore') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_DatabaseRestore AS RETURN 0;'); +GO +ALTER PROCEDURE [dbo].[sp_DatabaseRestore] + @Database NVARCHAR(128) = NULL, + @RestoreDatabaseName NVARCHAR(128) = NULL, + @BackupPathFull NVARCHAR(260) = NULL, + @BackupPathDiff NVARCHAR(260) = NULL, + @BackupPathLog NVARCHAR(260) = NULL, + @MoveFiles BIT = 1, + @MoveDataDrive NVARCHAR(260) = NULL, + @MoveLogDrive NVARCHAR(260) = NULL, + @MoveFilestreamDrive NVARCHAR(260) = NULL, + @MoveFullTextCatalogDrive NVARCHAR(260) = NULL, + @BufferCount INT = NULL, + @MaxTransferSize INT = NULL, + @BlockSize INT = NULL, + @TestRestore BIT = 0, + @RunCheckDB BIT = 0, + @RestoreDiff BIT = 0, + @ContinueLogs BIT = 0, + @StandbyMode BIT = 0, + @StandbyUndoPath NVARCHAR(MAX) = NULL, + @RunRecovery BIT = 0, + @ForceSimpleRecovery BIT = 0, + @ExistingDBAction TINYINT = 0, + @StopAt NVARCHAR(14) = NULL, + @OnlyLogsAfter NVARCHAR(14) = NULL, + @SimpleFolderEnumeration BIT = 0, + @SkipBackupsAlreadyInMsdb BIT = 0, + @DatabaseOwner sysname = NULL, + @SetTrustworthyON BIT = 0, + @FixOrphanUsers BIT = 0, + @KeepCdc BIT = 0, + @Execute CHAR(1) = Y, + @FileExtensionDiff NVARCHAR(128) = NULL, + @Debug INT = 0, + @Help BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @FileNamePrefix NVARCHAR(260) = NULL, + @RunStoredProcAfterRestore NVARCHAR(260) = NULL, + @EnableBroker BIT = 0 +AS +SET NOCOUNT ON; +SET STATISTICS XML OFF; -/*Sets up WHERE clause that gets used quite a bit*/ +/*Versioning details*/ ---Date stuff ---If they're both NULL, we'll just look at the last 7 days -IF (@StartDate IS NULL AND @EndDate IS NULL) - BEGIN - RAISERROR(N'@StartDate and @EndDate are NULL, checking last 7 days', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= DATEADD(DAY, -7, DATEDIFF(DAY, 0, SYSDATETIME() )) - '; - END; +SELECT @Version = '8.26', @VersionDate = '20251002'; ---Hey, that's nice of me -IF @StartDate IS NOT NULL - BEGIN - RAISERROR(N'Setting start date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= @sp_StartDate - '; - END; +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; ---Alright, sensible -IF @EndDate IS NOT NULL - BEGIN - RAISERROR(N'Setting end date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time < @sp_EndDate - '; - END; ---C'mon, why would you do that? -IF (@StartDate IS NULL AND @EndDate IS NOT NULL) - BEGIN - RAISERROR(N'Setting reasonable start date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= DATEADD(DAY, -7, @sp_EndDate) - '; - END; +IF @Help = 1 +BEGIN + PRINT ' + /* + sp_DatabaseRestore from http://FirstResponderKit.org ---Jeez, abusive -IF (@StartDate IS NOT NULL AND @EndDate IS NULL) - BEGIN - RAISERROR(N'Setting reasonable end date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time < DATEADD(DAY, 7, @sp_StartDate) - '; - END; + This script will restore a database from a given file path. ---I care about minimum execution counts -IF @MinimumExecutionCount IS NOT NULL - BEGIN - RAISERROR(N'Setting execution filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.count_executions >= @sp_MinimumExecutionCount - '; - END; + 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. ---You care about stored proc names -IF @StoredProcName IS NOT NULL - BEGIN - RAISERROR(N'Setting stored proc filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName - '; - END; + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Tastes awful with marmite. ---I will always love you, but hopefully this query will eventually end -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND (qsrs.avg_duration / 1000.) >= @sp_MinDuration - '; - END; + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) ---I don't know why you'd go looking for failed queries, but hey -IF (@Failed = 0 OR @Failed IS NULL) - BEGIN - RAISERROR(N'Setting failed query filter to 0', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.execution_type = 0 - '; - END; -IF (@Failed = 1) - BEGIN - RAISERROR(N'Setting failed query filter to 3, 4', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.execution_type IN (3, 4) - '; - END; + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ -/*Filtering for plan_id or query_id*/ -IF (@PlanIdFilter IS NOT NULL) - BEGIN - RAISERROR(N'Setting plan_id filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsp.plan_id = @sp_PlanIdFilter - '; - END; + MIT License -IF (@QueryIdFilter IS NOT NULL) - BEGIN - RAISERROR(N'Setting query_id filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsq.query_id = @sp_QueryIdFilter - '; - END; + Copyright (c) Brent Ozar Unlimited + 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. -IF @Debug = 1 - RAISERROR(N'Starting WHERE clause:', 0, 1) WITH NOWAIT; - PRINT @sql_where; + 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. -IF @sql_where IS NULL - BEGIN - RAISERROR(N'@sql_where is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; + */ + '; -IF (@ExportToExcel = 1 OR @SkipXML = 1) - BEGIN - RAISERROR(N'Exporting to Excel or skipping XML, hiding summary', 0, 1) WITH NOWAIT; - SET @HideSummary = 1; - END; + PRINT ' + /* + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 0, + @RunRecovery = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 1, + @RunRecovery = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 1, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 0, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @TestRestore = 1, + @RunCheckDB = 1, + @Debug = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @StandbyMode = 1, + @StandbyUndoPath = ''D:\Data\'', + @ContinueLogs = 1, + @RunRecovery = 0, + @Debug = 0; + + --Restore just through the latest DIFF, ignoring logs, and using a custom ".dif" file extension + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', + @RestoreDiff = 1, + @FileExtensionDiff = ''dif'', + @ContinueLogs = 0, + @RunRecovery = 1; + + -- Restore from stripped backup set when multiple paths are used. This example will restore stripped full backup set along with stripped transactional logs set from multiple backup paths + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''D:\Backup1\DBA\FULL,D:\Backup2\DBA\FULL'', + @BackupPathLog = ''D:\Backup1\DBA\LOG,D:\Backup2\DBA\LOG'', + @StandbyMode = 0, + @ContinueLogs = 1, + @RunRecovery = 0, + @Debug = 0; + + --This example will restore the latest differential backup, and stop transaction logs at the specified date time. It will execute and print debug information. + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @StopAt = ''20170508201501'', + @Debug = 1; + + --This example will NOT execute the restore. Commands will be printed in a copy/paste ready format only + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @TestRestore = 1, + @RunCheckDB = 1, + @Debug = 0, + @Execute = ''N''; + '; + + RETURN; +END; +-- Get the SQL Server version number because the columns returned by RESTORE commands vary by version +-- Based on: https://www.brentozar.com/archive/2015/05/sql-server-version-detection/ +-- Need to capture BuildVersion because RESTORE HEADERONLY changed with 2014 CU1, not RTM +DECLARE @ProductVersion AS NVARCHAR(20) = CAST(SERVERPROPERTY ('productversion') AS NVARCHAR(20)); +DECLARE @MajorVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 4) AS SMALLINT); +DECLARE @MinorVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 3) AS SMALLINT); +DECLARE @BuildVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 2) AS SMALLINT); -IF @StoredProcName IS NOT NULL - BEGIN - - DECLARE @sql NVARCHAR(MAX); - DECLARE @out INT; - DECLARE @proc_params NVARCHAR(MAX) = N'@sp_StartDate DATETIME2, @sp_EndDate DATETIME2, @sp_MinimumExecutionCount INT, @sp_MinDuration INT, @sp_StoredProcName NVARCHAR(128), @sp_PlanIdFilter INT, @sp_QueryIdFilter INT, @i_out INT OUTPUT'; - - - SET @sql = N'SELECT @i_out = COUNT(*) - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp - ON qsp.plan_id = qsrs.plan_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq - ON qsq.query_id = qsp.query_id - WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - - SET @sql += @sql_where; +IF @MajorVersion < 10 +BEGIN + RAISERROR('Sorry, DatabaseRestore doesn''t work on versions of SQL prior to 2008.', 15, 1); + RETURN; +END; - EXEC sys.sp_executesql @sql, - @proc_params, - @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter, @i_out = @out OUTPUT; - - IF @out = 0 - BEGIN +BEGIN TRY +DECLARE @CurrentDatabaseContext AS VARCHAR(128) = (SELECT DB_NAME()); +DECLARE @CommandExecuteCheck VARCHAR(400); - SET @msg = N'We couldn''t find the Stored Procedure ' + QUOTENAME(@StoredProcName) + N' in the Query Store views for ' + QUOTENAME(@DatabaseName) + N' between ' + CONVERT(NVARCHAR(30), ISNULL(@StartDate, DATEADD(DAY, -7, DATEDIFF(DAY, 0, SYSDATETIME() ))) ) + N' and ' + CONVERT(NVARCHAR(30), ISNULL(@EndDate, SYSDATETIME())) + - '. Try removing schema prefixes or adjusting dates. If it was executed from a different database context, try searching there instead.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; +SET @CommandExecuteCheck = 'IF NOT EXISTS (SELECT name FROM ' +@CurrentDatabaseContext+'.sys.objects WHERE type = ''P'' AND name = ''CommandExecute'') +BEGIN + RAISERROR (''DatabaseRestore requires the CommandExecute stored procedure from the OLA Hallengren Maintenance solution, are you using the correct database?'', 15, 1); + RETURN; +END;' +EXEC (@CommandExecuteCheck) +END TRY +BEGIN CATCH +THROW; +END CATCH - SELECT @msg AS [Blue Flowers, Blue Flowers, Blue Flowers]; - - RETURN; - - END; - - END; +DECLARE @cmd NVARCHAR(4000) = N'', --Holds xp_cmdshell command + @sql NVARCHAR(MAX) = N'', --Holds executable SQL commands + @LastFullBackup NVARCHAR(500) = N'', --Last full backup name + @LastDiffBackup NVARCHAR(500) = N'', --Last diff backup name + @LastDiffBackupDateTime NVARCHAR(500) = N'', --Last diff backup date + @BackupFile NVARCHAR(500) = N'', --Name of backup file + @BackupDateTime AS CHAR(15) = N'', --Used for comparisons to generate ordered backup files/create a stopat point + @FullLastLSN NUMERIC(25, 0), --LSN for full + @DiffLastLSN NUMERIC(25, 0), --LSN for diff + @HeadersSQL AS NVARCHAR(4000) = N'', --Dynamic insert into #Headers table (deals with varying results from RESTORE FILELISTONLY across different versions) + @MoveOption AS NVARCHAR(MAX) = N'', --If you need to move restored files to a different directory + @LogRecoveryOption AS NVARCHAR(MAX) = N'', --Holds the option to cause logs to be restored in standby mode or with no recovery + @DatabaseLastLSN NUMERIC(25, 0), --redo_start_lsn of the current database + @i TINYINT = 1, --Maintains loop to continue logs + @LogRestoreRanking INT = 1, --Holds Log iteration # when multiple paths & backup files are being stripped + @LogFirstLSN NUMERIC(25, 0), --Holds first LSN in log backup headers + @LogLastLSN NUMERIC(25, 0), --Holds last LSN in log backup headers + @LogLastNameInMsdbAS NVARCHAR(MAX) = N'', -- Holds last TRN file name already restored + @FileListParamSQL NVARCHAR(4000) = N'', --Holds INSERT list for #FileListParameters + @BackupParameters NVARCHAR(500) = N'', --Used to save BlockSize, MaxTransferSize and BufferCount + @RestoreDatabaseID SMALLINT, --Holds DB_ID of @RestoreDatabaseName + @UnquotedRestoreDatabaseName NVARCHAR(128); --Holds the unquoted @RestoreDatabaseName + +DECLARE @FileListSimple TABLE ( + BackupFile NVARCHAR(255) NOT NULL, + depth INT NOT NULL, + [file] INT NOT NULL +); +DECLARE @FileList TABLE ( + BackupPath NVARCHAR(255) NULL, + BackupFile NVARCHAR(255) NULL +); +DECLARE @PathItem TABLE ( + PathItem NVARCHAR(512) +); -/* -This is our grouped interval query. +IF OBJECT_ID(N'tempdb..#FileListParameters') IS NOT NULL DROP TABLE #FileListParameters; +CREATE TABLE #FileListParameters +( + LogicalName NVARCHAR(128) NOT NULL, + PhysicalName NVARCHAR(260) NOT NULL, + [Type] CHAR(1) NOT NULL, + FileGroupName NVARCHAR(120) NULL, + Size NUMERIC(20, 0) NOT NULL, + MaxSize NUMERIC(20, 0) NOT NULL, + FileID BIGINT NULL, + CreateLSN NUMERIC(25, 0) NULL, + DropLSN NUMERIC(25, 0) NULL, + UniqueID UNIQUEIDENTIFIER NULL, + ReadOnlyLSN NUMERIC(25, 0) NULL, + ReadWriteLSN NUMERIC(25, 0) NULL, + BackupSizeInBytes BIGINT NULL, + SourceBlockSize INT NULL, + FileGroupID INT NULL, + LogGroupGUID UNIQUEIDENTIFIER NULL, + DifferentialBaseLSN NUMERIC(25, 0) NULL, + DifferentialBaseGUID UNIQUEIDENTIFIER NULL, + IsReadOnly BIT NULL, + IsPresent BIT NULL, + TDEThumbprint VARBINARY(32) NULL, + SnapshotUrl NVARCHAR(360) NULL +); + +IF OBJECT_ID(N'tempdb..#Headers') IS NOT NULL DROP TABLE #Headers; +CREATE TABLE #Headers +( + BackupName NVARCHAR(256), + BackupDescription NVARCHAR(256), + BackupType NVARCHAR(256), + ExpirationDate NVARCHAR(256), + Compressed NVARCHAR(256), + Position NVARCHAR(256), + DeviceType NVARCHAR(256), + UserName NVARCHAR(256), + ServerName NVARCHAR(256), + DatabaseName NVARCHAR(256), + DatabaseVersion NVARCHAR(256), + DatabaseCreationDate NVARCHAR(256), + BackupSize NVARCHAR(256), + FirstLSN NVARCHAR(256), + LastLSN NVARCHAR(256), + CheckpointLSN NVARCHAR(256), + DatabaseBackupLSN NVARCHAR(256), + BackupStartDate NVARCHAR(256), + BackupFinishDate NVARCHAR(256), + SortOrder NVARCHAR(256), + [CodePage] NVARCHAR(256), + UnicodeLocaleId NVARCHAR(256), + UnicodeComparisonStyle NVARCHAR(256), + CompatibilityLevel NVARCHAR(256), + SoftwareVendorId NVARCHAR(256), + SoftwareVersionMajor NVARCHAR(256), + SoftwareVersionMinor NVARCHAR(256), + SoftwareVersionBuild NVARCHAR(256), + MachineName NVARCHAR(256), + Flags NVARCHAR(256), + BindingID NVARCHAR(256), + RecoveryForkID NVARCHAR(256), + Collation NVARCHAR(256), + FamilyGUID NVARCHAR(256), + HasBulkLoggedData NVARCHAR(256), + IsSnapshot NVARCHAR(256), + IsReadOnly NVARCHAR(256), + IsSingleUser NVARCHAR(256), + HasBackupChecksums NVARCHAR(256), + IsDamaged NVARCHAR(256), + BeginsLogChain NVARCHAR(256), + HasIncompleteMetaData NVARCHAR(256), + IsForceOffline NVARCHAR(256), + IsCopyOnly NVARCHAR(256), + FirstRecoveryForkID NVARCHAR(256), + ForkPointLSN NVARCHAR(256), + RecoveryModel NVARCHAR(256), + DifferentialBaseLSN NVARCHAR(256), + DifferentialBaseGUID NVARCHAR(256), + BackupTypeDescription NVARCHAR(256), + BackupSetGUID NVARCHAR(256), + CompressedBackupSize NVARCHAR(256), + Containment NVARCHAR(256), + KeyAlgorithm NVARCHAR(32), + EncryptorThumbprint VARBINARY(20), + EncryptorType NVARCHAR(32), + LastValidRestoreTime DATETIME, + TimeZone NVARCHAR(32), + CompressionAlgorithm NVARCHAR(32), + -- + -- Seq added to retain order by + -- + Seq INT NOT NULL IDENTITY(1, 1) +); -By default, it looks at queries: - In the last 7 days - That aren't system queries - That have a query plan (some won't, if nested level is > 128, along with other reasons) - And haven't failed - This stuff, along with some other options, will be configurable in the stored proc +/* +Correct paths in case people forget a final "\" or "/" +*/ +/*Full*/ +IF (SELECT RIGHT(@BackupPathFull, 1)) <> '/' AND CHARINDEX('/', @BackupPathFull) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathFull to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathFull += N'/'; +END; +ELSE IF (SELECT RIGHT(@BackupPathFull, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathFull to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathFull += N'\'; +END; +/*Diff*/ +IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '/' AND CHARINDEX('/', @BackupPathDiff) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathDiff to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathDiff += N'/'; +END; +ELSE IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathDiff to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathDiff += N'\'; +END; +/*Log*/ +IF (SELECT RIGHT(@BackupPathLog, 1)) <> '/' AND CHARINDEX('/', @BackupPathLog) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathLog to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathLog += N'/'; +END; +IF (SELECT RIGHT(@BackupPathLog, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathLog to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathLog += N'\'; +END; +/*Move Data File*/ +IF NULLIF(@MoveDataDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Getting default data drive for @MoveDataDrive', 0, 1) WITH NOWAIT; + SET @MoveDataDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '/' AND CHARINDEX('/', @MoveDataDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveDataDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveDataDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveDataDrive += N'\'; +END; +/*Move Log File*/ +IF NULLIF(@MoveLogDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Getting default log drive for @MoveLogDrive', 0, 1) WITH NOWAIT; + SET @MoveLogDrive = CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '/' AND CHARINDEX('/', @MoveLogDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing@MoveLogDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveLogDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveLogDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveLogDrive += N'\'; +END; +/*Move Filestream File*/ +IF NULLIF(@MoveFilestreamDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Setting default data drive for @MoveFilestreamDrive', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveFilestreamDrive, 1)) <> '/' AND CHARINDEX('/', @MoveFilestreamDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFilestreamDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveFilestreamDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFilestreamDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive += N'\'; +END; +/*Move FullText Catalog File*/ +IF NULLIF(@MoveFullTextCatalogDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Setting default data drive for @MoveFullTextCatalogDrive', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveFullTextCatalogDrive, 1)) <> '/' AND CHARINDEX('/', @MoveFullTextCatalogDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFullTextCatalogDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive += N'/'; +END; +IF (SELECT RIGHT(@MoveFullTextCatalogDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFullTextCatalogDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive += N'\'; +END; +/*Standby Undo File*/ +IF (SELECT RIGHT(@StandbyUndoPath, 1)) <> '/' AND CHARINDEX('/', @StandbyUndoPath) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @StandbyUndoPath to add a "/"', 0, 1) WITH NOWAIT; + SET @StandbyUndoPath += N'/'; +END; +IF (SELECT RIGHT(@StandbyUndoPath, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @StandbyUndoPath to add a "\"', 0, 1) WITH NOWAIT; + SET @StandbyUndoPath += N'\'; +END; -*/ +IF @RestoreDatabaseName IS NULL OR @RestoreDatabaseName LIKE N'' /*use LIKE instead of =, otherwise N'' = N' '. See: https://www.brentozar.com/archive/2017/04/surprising-behavior-trailing-spaces/ */ +BEGIN + SET @RestoreDatabaseName = @Database; +END; -IF @sql_where IS NOT NULL -BEGIN TRY +/*check input parameters*/ +IF NOT @MaxTransferSize IS NULL +BEGIN + IF @MaxTransferSize > 4194304 BEGIN + RAISERROR('@MaxTransferSize can not be greater then 4194304', 0, 1) WITH NOWAIT; + END - RAISERROR(N'Populating temp tables', 0, 1) WITH NOWAIT; - -RAISERROR(N'Gathering intervals', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT CONVERT(DATE, qsrs.last_execution_time) AS flat_date, - MIN(DATEADD(HOUR, DATEDIFF(HOUR, 0, qsrs.last_execution_time), 0)) AS start_range, - MAX(DATEADD(HOUR, DATEDIFF(HOUR, 0, qsrs.last_execution_time) + 1, 0)) AS end_range, - SUM(qsrs.avg_duration / 1000.) / SUM(qsrs.count_executions) AS total_avg_duration_ms, - SUM(qsrs.avg_cpu_time / 1000.) / SUM(qsrs.count_executions) AS total_avg_cpu_time_ms, - SUM((qsrs.avg_logical_io_reads * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_reads_mb, - SUM((qsrs.avg_physical_io_reads* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_physical_io_reads_mb, - SUM((qsrs.avg_logical_io_writes* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_writes_mb, - SUM(( qsrs.avg_query_max_used_memory * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_query_max_used_memory_mb, - SUM(qsrs.avg_rowcount) AS total_rowcount, - SUM(qsrs.count_executions) AS total_count_executions' - IF @new_columns = 1 - BEGIN - SET @sql_select += N', - SUM((qsrs.avg_log_bytes_used) / 1048576.) / SUM(qsrs.count_executions) AS total_avg_log_bytes_mb, - SUM(avg_tempdb_space_used) / SUM(qsrs.count_executions) AS total_avg_tempdb_space - ' - END - IF @new_columns = 0 - BEGIN - SET @sql_select += N', - NULL AS total_avg_log_bytes_mb, - NULL AS total_avg_tempdb_space - ' - END - - -SET @sql_select += N'FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp - ON qsp.plan_id = qsrs.plan_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq - ON qsq.query_id = qsp.query_id - WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - - -SET @sql_select += @sql_where; - -SET @sql_select += - N'GROUP BY CONVERT(DATE, qsrs.last_execution_time) - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #grouped_interval WITH (TABLOCK) - ( flat_date, start_range, end_range, total_avg_duration_ms, - total_avg_cpu_time_ms, total_avg_logical_io_reads_mb, total_avg_physical_io_reads_mb, - total_avg_logical_io_writes_mb, total_avg_query_max_used_memory_mb, total_rowcount, - total_count_executions, total_avg_log_bytes_mb, total_avg_tempdb_space ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - -/* -The next group of queries looks at plans in the ranges we found in the grouped interval query - -We take the highest value from each metric (duration, cpu, etc) and find the top plans by that metric in the range - -They insert into the #working_plans table -*/ + IF @MaxTransferSize % 64 <> 0 + BEGIN + RAISERROR('@MaxTransferSize has to be a multiple of 65536', 0, 1) WITH NOWAIT; + END +END; +IF NOT @BlockSize IS NULL +BEGIN + IF @BlockSize NOT IN (512, 1024, 2048, 4096, 8192, 16384, 32768, 65536) + BEGIN + RAISERROR('Supported values for @BlockSize are 512, 1024, 2048, 4096, 8192, 16384, 32768, and 65536', 0, 1) WITH NOWAIT; + END +END +--File Extension cleanup +IF @FileExtensionDiff LIKE '%.%' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Removing "." from @FileExtensionDiff', 0, 1) WITH NOWAIT; + SET @FileExtensionDiff = REPLACE(@FileExtensionDiff,'.',''); +END -/*Get longest duration plans*/ - -RAISERROR(N'Gathering longest duration plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH duration_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_duration_ms DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''duration'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN duration_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; +SET @RestoreDatabaseID = DB_ID(@RestoreDatabaseName); +SET @RestoreDatabaseName = QUOTENAME(@RestoreDatabaseName); +SET @UnquotedRestoreDatabaseName = PARSENAME(@RestoreDatabaseName,1); -SET @sql_select += @sql_where; +--If xp_cmdshell is disabled, force use of xp_dirtree +IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND value_in_use = 1) + SET @SimpleFolderEnumeration = 1; -SET @sql_select += N'ORDER BY qsrs.avg_duration DESC - OPTION (RECOMPILE); - '; +SET @HeadersSQL = +N'INSERT INTO #Headers WITH (TABLOCK) + (BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, Position, DeviceType, UserName, ServerName + ,DatabaseName, DatabaseVersion, DatabaseCreationDate, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN + ,BackupStartDate, BackupFinishDate, SortOrder, CodePage, UnicodeLocaleId, UnicodeComparisonStyle, CompatibilityLevel + ,SoftwareVendorId, SoftwareVersionMajor, SoftwareVersionMinor, SoftwareVersionBuild, MachineName, Flags, BindingID + ,RecoveryForkID, Collation, FamilyGUID, HasBulkLoggedData, IsSnapshot, IsReadOnly, IsSingleUser, HasBackupChecksums + ,IsDamaged, BeginsLogChain, HasIncompleteMetaData, IsForceOffline, IsCopyOnly, FirstRecoveryForkID, ForkPointLSN + ,RecoveryModel, DifferentialBaseLSN, DifferentialBaseGUID, BackupTypeDescription, BackupSetGUID, CompressedBackupSize'; -IF @Debug = 1 - PRINT @sql_select; +IF @MajorVersion >= 11 + SET @HeadersSQL += NCHAR(13) + NCHAR(10) + N', Containment'; -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; +IF @MajorVersion >= 13 OR (@MajorVersion = 12 AND @BuildVersion >= 2342) + SET @HeadersSQL += N', KeyAlgorithm, EncryptorThumbprint, EncryptorType'; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get longest cpu plans*/ - -RAISERROR(N'Gathering highest cpu plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH cpu_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_cpu_time_ms DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''cpu'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN cpu_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; +IF @MajorVersion >= 16 + SET @HeadersSQL += N', LastValidRestoreTime, TimeZone, CompressionAlgorithm'; -SET @sql_select += @sql_where; +SET @HeadersSQL += N')' + NCHAR(13) + NCHAR(10); +SET @HeadersSQL += N'EXEC (''RESTORE HEADERONLY FROM DISK=''''{Path}'''''')'; -SET @sql_select += N'ORDER BY qsrs.avg_cpu_time DESC - OPTION (RECOMPILE); - '; +IF @BackupPathFull IS NOT NULL +BEGIN + DECLARE @CurrentBackupPathFull NVARCHAR(255); -IF @Debug = 1 - PRINT @sql_select; + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathFull, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathFull IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, EndPosition + 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathFull, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, EndPosition + 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathFull ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 + BEGIN -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; + SELECT TOP 1 @CurrentBackupPathFull = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathFull, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest logical read plans*/ - -RAISERROR(N'Gathering highest logical read plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH logical_reads_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_logical_io_reads_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''logical reads'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN logical_reads_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @CurrentBackupPathFull, 1, 1; + INSERT @FileList (BackupPath,BackupFile) SELECT @CurrentBackupPathFull, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE + BEGIN + SET @cmd = N'DIR /b "' + @CurrentBackupPathFull + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathFull'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathFull + WHERE BackupPath IS NULL; + END; -SET @sql_select += @sql_where; + IF @Debug = 1 + BEGIN + SELECT BackupPath, BackupFile FROM @FileList; + END; + IF @SimpleFolderEnumeration = 1 + BEGIN + /*Check what we can*/ + IF NOT EXISTS (SELECT * FROM @FileList) + BEGIN + RAISERROR('(FULL) No rows were returned for that database in path %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + END + ELSE + BEGIN + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + OR fl.BackupFile = 'File Not Found' + ) = 1 + BEGIN + RAISERROR('(FULL) No rows or bad value for path %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(FULL) Access is denied to %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + ) = 1 + AND + ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile IS NULL + ) = 1 + BEGIN + RAISERROR('(FULL) Empty directory %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(FULL) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + END; + END + /*End folder sanity check*/ -SET @sql_select += N'ORDER BY qsrs.avg_logical_io_reads DESC - OPTION (RECOMPILE); - '; + IF @StopAt IS NOT NULL + BEGIN + DELETE + FROM @FileList + WHERE BackupFile LIKE N'%[_][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); -IF @Debug = 1 - PRINT @sql_select; + DELETE + FROM @FileList + 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); + END; -IF @sql_select IS NULL + -- Find latest full backup + -- Get the TOP record to use in "Restore HeaderOnly/FileListOnly" statement as well as Non-Split Backups Restore Command + SELECT TOP 1 @LastFullBackup = BackupFile, @CurrentBackupPathFull = BackupPath + FROM @FileList + WHERE BackupFile LIKE N'%.bak' + AND + BackupFile LIKE N'%' + @Database + N'%' + AND + (@StopAt IS NULL OR REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) <= @StopAt) + ORDER BY BackupFile DESC; + + /* To get all backups that belong to the same set we can do two things: + 1. RESTORE HEADERONLY of ALL backup files in the folder and look for BackupSetGUID. + Backups that belong to the same split will have the same BackupSetGUID. + 2. Olla Hallengren's solution appends file index at the end of the name: + SQLSERVER1_TEST_DB_FULL_20180703_213211_1.bak + SQLSERVER1_TEST_DB_FULL_20180703_213211_2.bak + SQLSERVER1_TEST_DB_FULL_20180703_213211_N.bak + We can and find all related files with the same timestamp but different index. + This option is simpler and requires less changes to this procedure */ + + IF @LastFullBackup IS NULL BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RAISERROR('No backups for "%s" found in "%s"', 16, 1, @Database, @BackupPathFull) WITH NOWAIT; RETURN; END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest physical read plans*/ - -RAISERROR(N'Gathering highest physical read plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH physical_read_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_physical_io_reads_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''physical reads'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN physical_read_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; + SELECT BackupPath, BackupFile INTO #SplitFullBackups + FROM @FileList + WHERE LEFT( BackupFile, LEN( BackupFile ) - PATINDEX( '%[_]%', REVERSE( BackupFile ) ) ) = LEFT( @LastFullBackup, LEN( @LastFullBackup ) - PATINDEX( '%[_]%', REVERSE( @LastFullBackup ) ) ) + AND PATINDEX( '%[_]%', REVERSE( @LastFullBackup ) ) <= 7 -- there is a 1 or 2 digit index at the end of the string which indicates split backups. Ola only supports up to 64 file split. + ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) DESC; -SET @sql_select += N'ORDER BY qsrs.avg_physical_io_reads DESC - OPTION (RECOMPILE); - '; + -- File list can be obtained by running RESTORE FILELISTONLY of any file from the given BackupSet therefore we do not have to cater for split backups when building @FileListParamSQL -IF @Debug = 1 - PRINT @sql_select; + SET @FileListParamSQL = + N'INSERT INTO #FileListParameters WITH (TABLOCK) + (LogicalName, PhysicalName, Type, FileGroupName, Size, MaxSize, FileID, CreateLSN, DropLSN + ,UniqueID, ReadOnlyLSN, ReadWriteLSN, BackupSizeInBytes, SourceBlockSize, FileGroupID, LogGroupGUID + ,DifferentialBaseLSN, DifferentialBaseGUID, IsReadOnly, IsPresent, TDEThumbprint'; -IF @sql_select IS NULL + IF @MajorVersion >= 13 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + SET @FileListParamSQL += N', SnapshotUrl'; END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest logical write plans*/ - -RAISERROR(N'Gathering highest write plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH logical_writes_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_logical_io_writes_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''writes'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN logical_writes_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; + SET @FileListParamSQL += N')' + NCHAR(13) + NCHAR(10); + SET @FileListParamSQL += N'EXEC (''RESTORE FILELISTONLY FROM DISK=''''{Path}'''''')'; -SET @sql_select += N'ORDER BY qsrs.avg_logical_io_writes DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; + SET @sql = REPLACE(@FileListParamSQL, N'{Path}', @CurrentBackupPathFull + @LastFullBackup); -IF @sql_select IS NULL + IF @Debug = 1 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + IF @sql IS NULL PRINT '@sql is NULL for INSERT to #FileListParameters: @BackupPathFull + @LastFullBackup'; + PRINT @sql; END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest memory use plans*/ - -RAISERROR(N'Gathering highest memory use plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH memory_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_query_max_used_memory_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''memory'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN memory_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_query_max_used_memory DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL + EXEC (@sql); + IF @Debug = 1 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest row count plans*/ - -RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_rowcount DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''rows'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_rowcount DESC - OPTION (RECOMPILE); - '; + SELECT '#FileListParameters' AS table_name, * FROM #FileListParameters; + SELECT '@FileList' AS table_name, BackupPath, BackupFile FROM @FileList WHERE BackupFile IS NOT NULL; + END -IF @Debug = 1 - PRINT @sql_select; + --get the backup completed data so we can apply tlogs from that point forwards + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathFull + @LastFullBackup); -IF @sql_select IS NULL + IF @Debug = 1 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + IF @sql IS NULL PRINT '@sql is NULL for get backup completed data: @BackupPathFull, @LastFullBackup'; + PRINT @sql; END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - -IF @new_columns = 1 -BEGIN - -RAISERROR(N'Gathering new 2017 new column info...', 0, 1) WITH NOWAIT; - -/*Get highest log byte count plans*/ - -RAISERROR(N'Gathering highest log byte use plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_log_bytes_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''log bytes'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_log_bytes_used DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL + EXECUTE (@sql); + IF @Debug = 1 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + SELECT '#Headers' AS table_name, @LastFullBackup AS FullBackupFile, * FROM #Headers END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -/*Get highest row count plans*/ - -RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_tempdb_space DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''tempdb space'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_tempdb_space_used DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL + --Ensure we are looking at the expected backup, but only if we expect to restore a FULL backups + IF NOT EXISTS (SELECT * FROM #Headers h WHERE h.DatabaseName = @Database) BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; + RAISERROR('Backupfile "%s" does not match @Database parameter "%s"', 16, 1, @LastFullBackup, @Database) WITH NOWAIT; RETURN; END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -END; - - -/* -This rolls up the different patterns we find before deduplicating. - -The point of this is so we know if a query was gathered by one or more of the search queries - -*/ - -RAISERROR(N'Updating patterns', 0, 1) WITH NOWAIT; - -WITH patterns AS ( -SELECT wp.plan_id, wp.query_id, - pattern_path = STUFF((SELECT DISTINCT N', ' + wp2.pattern - FROM #working_plans AS wp2 - WHERE wp.plan_id = wp2.plan_id - AND wp.query_id = wp2.query_id - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') -FROM #working_plans AS wp -) -UPDATE wp -SET wp.pattern = patterns.pattern_path -FROM #working_plans AS wp -JOIN patterns -ON wp.plan_id = patterns.plan_id -AND wp.query_id = patterns.query_id -OPTION (RECOMPILE); - - -/* -This dedupes our results so we hopefully don't double-work the same plan -*/ - -RAISERROR(N'Deduplicating gathered plans', 0, 1) WITH NOWAIT; - -WITH dedupe AS ( -SELECT * , ROW_NUMBER() OVER (PARTITION BY wp.plan_id ORDER BY wp.plan_id) AS dupes -FROM #working_plans AS wp -) -DELETE dedupe -WHERE dedupe.dupes > 1 -OPTION (RECOMPILE); - -SET @msg = N'Removed ' + CONVERT(NVARCHAR(10), @@ROWCOUNT) + N' duplicate plan_ids.'; -RAISERROR(@msg, 0, 1) WITH NOWAIT; - - -/* -This gathers data for the #working_metrics table -*/ - - -RAISERROR(N'Collecting worker metrics', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT ' + QUOTENAME(@DatabaseName, '''') + N' AS database_name, wp.plan_id, wp.query_id, - QUOTENAME(object_schema_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) + ''.'' + - QUOTENAME(object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) AS proc_or_function_name, - qsq.batch_sql_handle, qsq.query_hash, qsq.query_parameterization_type_desc, qsq.count_compiles, - (qsq.avg_compile_duration / 1000.), - (qsq.last_compile_duration / 1000.), - (qsq.avg_bind_duration / 1000.), - (qsq.last_bind_duration / 1000.), - (qsq.avg_bind_cpu_time / 1000.), - (qsq.last_bind_cpu_time / 1000.), - (qsq.avg_optimize_duration / 1000.), - (qsq.last_optimize_duration / 1000.), - (qsq.avg_optimize_cpu_time / 1000.), - (qsq.last_optimize_cpu_time / 1000.), - (qsq.avg_compile_memory_kb / 1024.), - (qsq.last_compile_memory_kb / 1024.), - qsrs.execution_type_desc, qsrs.first_execution_time, qsrs.last_execution_time, qsrs.count_executions, - (qsrs.avg_duration / 1000.), - (qsrs.last_duration / 1000.), - (qsrs.min_duration / 1000.), - (qsrs.max_duration / 1000.), - (qsrs.avg_cpu_time / 1000.), - (qsrs.last_cpu_time / 1000.), - (qsrs.min_cpu_time / 1000.), - (qsrs.max_cpu_time / 1000.), - ((qsrs.avg_logical_io_reads * 8 ) / 1024.), - ((qsrs.last_logical_io_reads * 8 ) / 1024.), - ((qsrs.min_logical_io_reads * 8 ) / 1024.), - ((qsrs.max_logical_io_reads * 8 ) / 1024.), - ((qsrs.avg_logical_io_writes * 8 ) / 1024.), - ((qsrs.last_logical_io_writes * 8 ) / 1024.), - ((qsrs.min_logical_io_writes * 8 ) / 1024.), - ((qsrs.max_logical_io_writes * 8 ) / 1024.), - ((qsrs.avg_physical_io_reads * 8 ) / 1024.), - ((qsrs.last_physical_io_reads * 8 ) / 1024.), - ((qsrs.min_physical_io_reads * 8 ) / 1024.), - ((qsrs.max_physical_io_reads * 8 ) / 1024.), - (qsrs.avg_clr_time / 1000.), - (qsrs.last_clr_time / 1000.), - (qsrs.min_clr_time / 1000.), - (qsrs.max_clr_time / 1000.), - qsrs.avg_dop, qsrs.last_dop, qsrs.min_dop, qsrs.max_dop, - ((qsrs.avg_query_max_used_memory * 8 ) / 1024.), - ((qsrs.last_query_max_used_memory * 8 ) / 1024.), - ((qsrs.min_query_max_used_memory * 8 ) / 1024.), - ((qsrs.max_query_max_used_memory * 8 ) / 1024.), - qsrs.avg_rowcount, qsrs.last_rowcount, qsrs.min_rowcount, qsrs.max_rowcount,'; - - IF @new_columns = 1 - BEGIN - SET @sql_select += N' - qsrs.avg_num_physical_io_reads, qsrs.last_num_physical_io_reads, qsrs.min_num_physical_io_reads, qsrs.max_num_physical_io_reads, - (qsrs.avg_log_bytes_used / 100000000), - (qsrs.last_log_bytes_used / 100000000), - (qsrs.min_log_bytes_used / 100000000), - (qsrs.max_log_bytes_used / 100000000), - ((qsrs.avg_tempdb_space_used * 8 ) / 1024.), - ((qsrs.last_tempdb_space_used * 8 ) / 1024.), - ((qsrs.min_tempdb_space_used * 8 ) / 1024.), - ((qsrs.max_tempdb_space_used * 8 ) / 1024.) - '; - END; - IF @new_columns = 0 - BEGIN - SET @sql_select += N' - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - '; - END; -SET @sql_select += -N'FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id -AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; + IF NOT @BufferCount IS NULL + BEGIN + SET @BackupParameters += N', BufferCount=' + cast(@BufferCount as NVARCHAR(10)) + END -SET @sql_select += N'OPTION (RECOMPILE); - '; + IF NOT @MaxTransferSize IS NULL + BEGIN + SET @BackupParameters += N', MaxTransferSize=' + cast(@MaxTransferSize as NVARCHAR(7)) + END -IF @Debug = 1 - PRINT @sql_select; + IF NOT @BlockSize IS NULL + BEGIN + SET @BackupParameters += N', BlockSize=' + cast(@BlockSize as NVARCHAR(5)) + END -IF @sql_select IS NULL + IF @MoveFiles = 1 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #working_metrics WITH (TABLOCK) - ( database_name, plan_id, query_id, - proc_or_function_name, - batch_sql_handle, query_hash, query_parameterization_type_desc, count_compiles, - avg_compile_duration, last_compile_duration, avg_bind_duration, last_bind_duration, avg_bind_cpu_time, last_bind_cpu_time, avg_optimize_duration, - last_optimize_duration, avg_optimize_cpu_time, last_optimize_cpu_time, avg_compile_memory_kb, last_compile_memory_kb, execution_type_desc, - first_execution_time, last_execution_time, count_executions, avg_duration, last_duration, min_duration, max_duration, avg_cpu_time, last_cpu_time, - min_cpu_time, max_cpu_time, avg_logical_io_reads, last_logical_io_reads, min_logical_io_reads, max_logical_io_reads, avg_logical_io_writes, - last_logical_io_writes, min_logical_io_writes, max_logical_io_writes, avg_physical_io_reads, last_physical_io_reads, min_physical_io_reads, - max_physical_io_reads, avg_clr_time, last_clr_time, min_clr_time, max_clr_time, avg_dop, last_dop, min_dop, max_dop, avg_query_max_used_memory, - last_query_max_used_memory, min_query_max_used_memory, max_query_max_used_memory, avg_rowcount, last_rowcount, min_rowcount, max_rowcount, - /* 2017 only columns */ - avg_num_physical_io_reads, last_num_physical_io_reads, min_num_physical_io_reads, max_num_physical_io_reads, - avg_log_bytes_used, last_log_bytes_used, min_log_bytes_used, max_log_bytes_used, - avg_tempdb_space_used, last_tempdb_space_used, min_tempdb_space_used, max_tempdb_space_used ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*This just helps us classify our queries*/ -UPDATE #working_metrics -SET proc_or_function_name = N'Statement' -WHERE proc_or_function_name IS NULL; - - -/* -This gathers data for the #working_plan_text table -*/ + IF @Execute = 'Y' RAISERROR('@MoveFiles = 1, adjusting paths', 0, 1) WITH NOWAIT; - -RAISERROR(N'Gathering working plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT ' + QUOTENAME(@DatabaseName, '''') + N' AS database_name, wp.plan_id, wp.query_id, - qsp.plan_group_id, qsp.engine_version, qsp.compatibility_level, qsp.query_plan_hash, TRY_CONVERT(XML, qsp.query_plan), qsp.is_online_index_plan, qsp.is_trivial_plan, - qsp.is_parallel_plan, qsp.is_forced_plan, qsp.is_natively_compiled, qsp.force_failure_count, qsp.last_force_failure_reason_desc, qsp.count_compiles, - qsp.initial_compile_start_time, qsp.last_compile_start_time, qsp.last_execution_time, - (qsp.avg_compile_duration / 1000.), - (qsp.last_compile_duration / 1000.), - qsqt.query_sql_text, qsqt.statement_sql_handle, qsqt.is_part_of_encrypted_module, qsqt.has_restricted_text -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + WITH Files + AS ( + SELECT + CASE + WHEN Type = 'D' THEN @MoveDataDrive + WHEN Type = 'L' THEN @MoveLogDrive + WHEN Type = 'S' THEN @MoveFilestreamDrive + WHEN Type = 'F' THEN @MoveFullTextCatalogDrive + END + COALESCE(@FileNamePrefix, '') + CASE + WHEN @Database = @RestoreDatabaseName THEN REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)) + ELSE REPLACE(REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)), @Database, SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) -2)) + END AS TargetPhysicalName, + PhysicalName, + LogicalName + FROM #FileListParameters) + SELECT @MoveOption = @MoveOption + N', MOVE ''' + Files.LogicalName + N''' TO ''' + Files.TargetPhysicalName + '''' + FROM Files + WHERE Files.TargetPhysicalName <> Files.PhysicalName; + + IF @Debug = 1 PRINT @MoveOption END; -INSERT #working_plan_text WITH (TABLOCK) - ( database_name, plan_id, query_id, - plan_group_id, engine_version, compatibility_level, query_plan_hash, query_plan_xml, is_online_index_plan, is_trivial_plan, - is_parallel_plan, is_forced_plan, is_natively_compiled, force_failure_count, last_force_failure_reason_desc, count_compiles, - initial_compile_start_time, last_compile_start_time, last_execution_time, avg_compile_duration, last_compile_duration, - query_sql_text, statement_sql_handle, is_part_of_encrypted_module, has_restricted_text ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/* - -Some memory grant information isn't available in query store - -We have to go back to other DMVs to find it, when possible - -It may not be there for various reaons - -*/ -RAISERROR(N'Checking dm_exec_query_stats for memory grant info', 0, 1) WITH NOWAIT; -WITH max_mem -AS ( SELECT deqs.sql_handle, MAX(deqs.min_grant_kb) AS min_grant_kb, MAX(deqs.max_used_grant_kb) AS max_used_grant_kb - FROM sys.dm_exec_query_stats AS deqs - GROUP BY deqs.sql_handle ) -UPDATE wpt -SET wpt.min_grant_kb = deqs.min_grant_kb, - wpt.max_used_grant_kb = deqs.max_used_grant_kb -FROM #working_plan_text AS wpt -JOIN max_mem AS deqs -ON wpt.statement_sql_handle = deqs.sql_handle -OPTION (RECOMPILE); - - -/* -This gets us context settings for our queries and adds it to the #working_plan_text table -*/ - -RAISERROR(N'Gathering context settings', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -UPDATE wp -SET wp.context_settings = SUBSTRING( - CASE WHEN (CAST(qcs.set_options AS INT) & 1 = 1) THEN '', ANSI_PADDING'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 8 = 8) THEN '', CONCAT_NULL_YIELDS_NULL'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 16 = 16) THEN '', ANSI_WARNINGS'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 32 = 32) THEN '', ANSI_NULLS'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 64 = 64) THEN '', QUOTED_IDENTIFIER'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 4096 = 4096) THEN '', ARITH_ABORT'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 8192 = 8192) THEN '', NUMERIC_ROUNDABORT'' ELSE '''' END - , 2, 200000) -FROM #working_plan_text wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_context_settings AS qcs -ON qcs.context_settings_id = qsq.context_settings_id -OPTION (RECOMPILE); -'; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL + /*Process @ExistingDBAction flag */ + IF @ExistingDBAction BETWEEN 1 AND 4 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select; - - -/*This adds the patterns we found from each interval to the #working_plan_text table*/ - -RAISERROR(N'Add patterns to working plans', 0, 1) WITH NOWAIT; - -UPDATE wpt -SET wpt.pattern = wp.pattern -FROM #working_plans AS wp -JOIN #working_plan_text AS wpt -ON wpt.plan_id = wp.plan_id -AND wpt.query_id = wp.query_id -OPTION (RECOMPILE); + IF @RestoreDatabaseID IS NOT NULL + BEGIN + IF @ExistingDBAction = 1 + BEGIN + RAISERROR('Setting single user', 0, 1) WITH NOWAIT; + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE; ' + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for SINGLE_USER'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') != 'RESTORING' + BEGIN + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'ALTER DATABASE SINGLE_USER', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE IF @Debug = 1 + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') IS NULL PRINT 'Unable to retrieve STATUS from "' + @UnquotedRestoreDatabaseName + '" database. Skipping setting database to SINGLE_USER'; + ELSE IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') = 'RESTORING' PRINT @UnquotedRestoreDatabaseName + ' database STATUS is RESTORING. Skiping setting database to SINGLE_USER'; + END + END + END + IF @ExistingDBAction IN (2, 3) + BEGIN + RAISERROR('Killing connections', 0, 1) WITH NOWAIT; + SET @sql = N'/* Kill connections */' + NCHAR(13); + SELECT + @sql = @sql + N'KILL ' + CAST(spid as nvarchar(5)) + N';' + NCHAR(13) + FROM + --database_ID was only added to sys.dm_exec_sessions in SQL Server 2012 but we need to support older + sys.sysprocesses + WHERE + dbid = @RestoreDatabaseID; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Kill connections'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'KILL CONNECTIONS', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + IF @ExistingDBAction = 3 + BEGIN + RAISERROR('Dropping database', 0, 1) WITH NOWAIT; + + SET @sql = N'DROP DATABASE ' + @RestoreDatabaseName + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + IF @ExistingDBAction = 4 + BEGIN + RAISERROR ('Offlining database', 0, 1) WITH NOWAIT; -/*This cleans up query text a bit*/ + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + SPACE( 1 ) + 'SET OFFLINE WITH ROLLBACK IMMEDIATE'; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Offline database'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') != 'RESTORING' + BEGIN + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'OFFLINE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE IF @Debug = 1 + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') IS NULL PRINT 'Unable to retrieve STATUS from "' + @UnquotedRestoreDatabaseName + '" database. Skipping setting database OFFLINE'; + ELSE IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') = 'RESTORING' PRINT @UnquotedRestoreDatabaseName + ' database STATUS is RESTORING. Skiping setting database OFFLINE'; + END + END + END; + END + ELSE + RAISERROR('@ExistingDBAction > 0, but no existing @RestoreDatabaseName', 0, 1) WITH NOWAIT; + END + ELSE + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@ExistingDBAction %u so do nothing', 0, 1, @ExistingDBAction) WITH NOWAIT; -RAISERROR(N'Clean awkward characters from query text', 0, 1) WITH NOWAIT; + IF @ContinueLogs = 0 + BEGIN + IF @Execute = 'Y' RAISERROR('@ContinueLogs set to 0', 0, 1) WITH NOWAIT; -UPDATE b -SET b.query_sql_text = REPLACE(REPLACE(REPLACE(b.query_sql_text, @cr, ' '), @lf, ' '), @tab, ' ') -FROM #working_plan_text AS b -OPTION (RECOMPILE); + /* now take split backups into account */ + IF (SELECT COUNT(*) FROM #SplitFullBackups) > 0 + BEGIN + IF @Debug = 1 RAISERROR('Split backups found', 0, 1) WITH NOWAIT; + + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitFullBackups + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '') + N' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + ELSE + BEGIN + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathFull + @LastFullBackup + N''' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END + IF (@StandbyMode = 1) + BEGIN + IF (@StandbyUndoPath IS NULL) + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. The database will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END + ELSE IF (SELECT COUNT(*) FROM #SplitFullBackups) > 0 + BEGIN + SET @sql = @sql + ', STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + END + ELSE + BEGIN + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathFull + @LastFullBackup + N''' WITH REPLACE' + @BackupParameters + @MoveOption + N' , STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + END + END; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathFull, @LastFullBackup, @MoveOption'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; -/*This populates #working_wait_stats when available*/ + -- We already loaded #Headers above -IF @waitstats = 1 + --setting the @BackupDateTime to a numeric string so that it can be used in comparisons + SET @BackupDateTime = REPLACE( RIGHT( REPLACE( @LastFullBackup, RIGHT( @LastFullBackup, PATINDEX( '%_[0-9][0-9]%', REVERSE( @LastFullBackup ) ) ), '' ), 16 ), '_', '' ); - BEGIN - - RAISERROR(N'Collecting wait stats info', 0, 1) WITH NOWAIT; - - - SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; - SET @sql_select += N' - SELECT qws.plan_id, - qws.wait_category, - qws.wait_category_desc, - SUM(qws.total_query_wait_time_ms) AS total_query_wait_time_ms, - SUM(qws.avg_query_wait_time_ms) AS avg_query_wait_time_ms, - SUM(qws.last_query_wait_time_ms) AS last_query_wait_time_ms, - SUM(qws.min_query_wait_time_ms) AS min_query_wait_time_ms, - SUM(qws.max_query_wait_time_ms) AS max_query_wait_time_ms - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_wait_stats qws - JOIN #working_plans AS wp - ON qws.plan_id = wp.plan_id - GROUP BY qws.plan_id, qws.wait_category, qws.wait_category_desc - HAVING SUM(qws.min_query_wait_time_ms) >= 5 - OPTION (RECOMPILE); - '; - + SELECT @FullLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) FROM #Headers WHERE BackupType = 1; IF @Debug = 1 - PRINT @sql_select; - - IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - - INSERT #working_wait_stats WITH (TABLOCK) - ( plan_id, wait_category, wait_category_desc, total_query_wait_time_ms, avg_query_wait_time_ms, last_query_wait_time_ms, min_query_wait_time_ms, max_query_wait_time_ms ) - - EXEC sys.sp_executesql @stmt = @sql_select; - - - /*This updates #working_plan_text with the top three waits from the wait stats DMV*/ - - RAISERROR(N'Update working_plan_text with top three waits', 0, 1) WITH NOWAIT; - - - UPDATE wpt - SET wpt.top_three_waits = x.top_three_waits - FROM #working_plan_text AS wpt - JOIN ( - SELECT wws.plan_id, - top_three_waits = STUFF((SELECT TOP 3 N', ' + wws2.wait_category_desc + N' (' + CONVERT(NVARCHAR(20), SUM(CONVERT(BIGINT, wws2.avg_query_wait_time_ms))) + N' ms) ' - FROM #working_wait_stats AS wws2 - WHERE wws.plan_id = wws2.plan_id - GROUP BY wws2.wait_category_desc - ORDER BY SUM(wws2.avg_query_wait_time_ms) DESC - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') - FROM #working_wait_stats AS wws - GROUP BY wws.plan_id - ) AS x - ON x.plan_id = wpt.plan_id - OPTION (RECOMPILE); - -END; + BEGIN + IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastFullBackup'; + PRINT @BackupDateTime; + END; -/*End wait stats population*/ + END; + ELSE + BEGIN -UPDATE #working_plan_text -SET top_three_waits = CASE - WHEN @waitstats = 0 THEN N'The query store waits stats DMV is not available' - ELSE N'No Significant waits detected!' - END -WHERE top_three_waits IS NULL; + SELECT @DatabaseLastLSN = CAST(f.redo_start_lsn AS NUMERIC(25, 0)) + FROM master.sys.databases d + JOIN master.sys.master_files f ON d.database_id = f.database_id + WHERE d.name = SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) - 2) AND f.file_id = 1; + END; END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; -IF (@SkipXML = 0) -BEGIN TRY +IF @BackupPathFull IS NULL AND @ContinueLogs = 1 BEGIN -/* -This sets up the #working_warnings table with the IDs we're interested in so we can tie warnings back to them -*/ + SELECT @DatabaseLastLSN = CAST(f.redo_start_lsn AS NUMERIC(25, 0)) + FROM master.sys.databases d + JOIN master.sys.master_files f ON d.database_id = f.database_id + WHERE d.name = SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) - 2) AND f.file_id = 1; -RAISERROR(N'Populate working warnings table with gathered plans', 0, 1) WITH NOWAIT; - - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT wp.plan_id, wp.query_id, qsq.query_hash, qsqt.statement_sql_handle -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; +END; -SET @sql_select += @sql_where; +IF @BackupPathDiff IS NOT NULL +BEGIN + DELETE FROM @FileList; + DELETE FROM @FileListSimple; + DELETE FROM @PathItem; + + DECLARE @CurrentBackupPathDiff NVARCHAR(512); + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathDiff, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathDiff IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, EndPosition + 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathDiff, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, EndPosition + 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathDiff ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 + BEGIN -SET @sql_select += N'OPTION (RECOMPILE); - '; + SELECT TOP 1 @CurrentBackupPathDiff = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathDiff, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; -IF @Debug = 1 - PRINT @sql_select; + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @CurrentBackupPathDiff, 1, 1; + INSERT @FileList (BackupPath,BackupFile) SELECT @CurrentBackupPathDiff, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE + BEGIN + SET @cmd = N'DIR /b "' + @CurrentBackupPathDiff + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathDiff'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathDiff WHERE BackupPath IS NULL; + END; -IF @sql_select IS NULL + IF @Debug = 1 + BEGIN + SELECT BackupPath,BackupFile FROM @FileList WHERE BackupFile IS NOT NULL; + END; + IF @SimpleFolderEnumeration = 0 BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Bad value for path %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Access is denied to %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; END; + END + /*End folder sanity check*/ + -- Find latest diff backup + IF @FileExtensionDiff IS NULL + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('No @FileExtensionDiff given, assuming "bak".', 0, 1) WITH NOWAIT; + SET @FileExtensionDiff = 'bak'; + END -INSERT #working_warnings WITH (TABLOCK) - ( plan_id, query_id, query_hash, sql_handle ) -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + SELECT TOP 1 @LastDiffBackup = BackupFile, @CurrentBackupPathDiff = BackupPath + FROM @FileList + WHERE BackupFile LIKE N'%.' + @FileExtensionDiff + AND + BackupFile LIKE N'%' + @Database + '%' + AND + (@StopAt IS NULL OR REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) <= @StopAt) + ORDER BY BackupFile DESC; + + -- Load FileList data into Temp Table sorted by DateTime Stamp desc + SELECT BackupPath, BackupFile INTO #SplitDiffBackups + FROM @FileList + WHERE LEFT( BackupFile, LEN( BackupFile ) - PATINDEX( '%[_]%', REVERSE( BackupFile ) ) ) = LEFT( @LastDiffBackup, LEN( @LastDiffBackup ) - PATINDEX( '%[_]%', REVERSE( @LastDiffBackup ) ) ) + AND PATINDEX( '%[_]%', REVERSE( @LastDiffBackup ) ) <= 7 -- there is a 1 or 2 digit index at the end of the string which indicates split backups. Olla only supports up to 64 file split. + ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) DESC; + + --No file = no backup to restore + SET @LastDiffBackupDateTime = REPLACE( RIGHT( REPLACE( @LastDiffBackup, RIGHT( @LastDiffBackup, PATINDEX( '%_[0-9][0-9]%', REVERSE( @LastDiffBackup ) ) ), '' ), 16 ), '_', '' ); + + IF @RestoreDiff = 1 AND @BackupDateTime < @LastDiffBackupDateTime + BEGIN -/* -This looks for queries in the query stores that we picked up from an internal that have multiple plans in cache + IF (SELECT COUNT(*) FROM #SplitDiffBackups) > 0 + BEGIN + IF @Debug = 1 RAISERROR ('Split backups found', 0, 1) WITH NOWAIT; + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitDiffBackups + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '' ) + N' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + ELSE + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathDiff + @LastDiffBackup + N''' WITH NORECOVERY' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); -This and several of the following queries all replaced XML parsing to find plan attributes. Sweet. + IF (@StandbyMode = 1) + BEGIN + IF (@StandbyUndoPath IS NULL) + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. The database will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END + ELSE IF (SELECT COUNT(*) FROM #SplitDiffBackups) > 0 + SET @sql = @sql + ', STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + ELSE + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathDiff, @LastDiffBackup'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; -Thanks, Query Store -*/ + --get the backup completed data so we can apply tlogs from that point forwards + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathDiff + @LastDiffBackup); -RAISERROR(N'Populating object name in #working_warnings', 0, 1) WITH NOWAIT; -UPDATE w -SET w.proc_or_function_name = ISNULL(wm.proc_or_function_name, N'Statement') -FROM #working_warnings AS w -JOIN #working_metrics AS wm -ON w.plan_id = wm.plan_id - AND w.query_id = wm.query_id -OPTION (RECOMPILE); + IF @Debug = 1 + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @CurrentBackupPathDiff, @LastDiffBackup'; + PRINT @sql; + END; + EXECUTE (@sql); + IF @Debug = 1 + BEGIN + SELECT '#Headers' AS table_name, @LastDiffBackup AS DiffbackupFile, * FROM #Headers AS h WHERE h.BackupType = 5; + END -RAISERROR(N'Checking for multiple plans', 0, 1) WITH NOWAIT; + --set the @BackupDateTime to the date time on the most recent differential + SET @BackupDateTime = ISNULL( @LastDiffBackupDateTime, @BackupDateTime ); + IF @Debug = 1 + BEGIN + IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastDiffBackupDateTime'; + PRINT @BackupDateTime; + END; + SELECT @DiffLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) + FROM #Headers + WHERE BackupType = 5; + END; -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -UPDATE ww -SET ww.plan_multiple_plans = 1 -FROM #working_warnings AS ww -JOIN -( -SELECT wp.query_id, COUNT(qsp.plan_id) AS plans -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; + IF @DiffLastLSN IS NULL + BEGIN + SET @DiffLastLSN=@FullLastLSN + END +END -SET @sql_select += @sql_where; -SET @sql_select += N'GROUP BY wp.query_id - HAVING COUNT(qsp.plan_id) > 1 - ) AS x - ON ww.query_id = x.query_id - OPTION (RECOMPILE); - '; -IF @Debug = 1 - PRINT @sql_select; +IF @BackupPathLog IS NOT NULL +BEGIN + DELETE FROM @FileList; + DELETE FROM @FileListSimple; + DELETE FROM @PathItem; + + DECLARE @CurrentBackupPathLog NVARCHAR(512); + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathLog, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathLog IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, EndPosition + 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathLog, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, EndPosition + 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathLog ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 + BEGIN + SELECT TOP 1 @CurrentBackupPathLog = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathLog, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; + + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @BackupPathLog, 1, 1; + INSERT @FileList (BackupPath, BackupFile) SELECT @CurrentBackupPathLog, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE + BEGIN + SET @cmd = N'DIR /b "' + @CurrentBackupPathLog + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathLog'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathLog + WHERE BackupPath IS NULL; + END; -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; + IF @SimpleFolderEnumeration = 1 + BEGIN + /*Check what we can*/ + IF NOT EXISTS (SELECT * FROM @FileList) + BEGIN + RAISERROR('(LOG) No rows were returned for that database %s in path %s', 16, 1, @Database, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; + END + ELSE + BEGIN + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + OR fl.BackupFile = 'File Not Found' + ) = 1 + BEGIN + RAISERROR('(LOG) No rows or bad value for path %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(LOG) Access is denied to %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; -/* -This looks for forced plans -*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + ) = 1 + AND + ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile IS NULL + ) = 1 + BEGIN + RAISERROR('(LOG) Empty directory %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END -RAISERROR(N'Checking for forced plans', 0, 1) WITH NOWAIT; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(LOG) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; + END; + END + /*End folder sanity check*/ -UPDATE ww -SET ww.is_forced_plan = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.is_forced_plan = 1 -OPTION (RECOMPILE); +IF @Debug = 1 +BEGIN + SELECT * FROM @FileList WHERE BackupFile IS NOT NULL; +END +IF @SkipBackupsAlreadyInMsdb = 1 +BEGIN -/* -This looks for forced parameterization -*/ + SELECT TOP 1 @LogLastNameInMsdbAS = bf.physical_device_name + FROM msdb.dbo.backupmediafamily bf + INNER JOIN msdb.dbo.backupset bs ON bs.media_set_id = bf.media_set_id + INNER JOIN msdb.dbo.restorehistory rh ON rh.backup_set_id = bs.backup_set_id + WHERE physical_device_name like @BackupPathLog + '%' + AND rh.destination_database_name = @UnquotedRestoreDatabaseName + ORDER BY physical_device_name DESC -RAISERROR(N'Checking for forced parameterization', 0, 1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + SELECT 'Keeping LOG backups with name > : ' + @LogLastNameInMsdbAS + END -UPDATE ww -SET ww.is_forced_parameterized = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.query_parameterization_type_desc = 'Forced' -OPTION (RECOMPILE); + DELETE fl + FROM @FileList AS fl + WHERE fl.BackupPath + fl.BackupFile <= @LogLastNameInMsdbAS +END -/* -This looks for unparameterized queries -*/ -RAISERROR(N'Checking for unparameterized plans', 0, 1) WITH NOWAIT; +IF (@OnlyLogsAfter IS NOT NULL) +BEGIN -UPDATE ww -SET ww.unparameterized_query = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.query_parameterization_type_desc = 'None' - AND ww.proc_or_function_name = 'Statement' -OPTION (RECOMPILE); + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@OnlyLogsAfter is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) < @OnlyLogsAfter; -/* -This looks for cursors -*/ +END -RAISERROR(N'Checking for cursors', 0, 1) WITH NOWAIT; +-- Check for log backups +IF(@BackupDateTime IS NOT NULL AND @BackupDateTime <> '') + BEGIN + DELETE FROM @FileList + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND NOT (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) >= @BackupDateTime)); + END; -UPDATE ww -SET ww.is_cursor = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.plan_group_id > 0 -OPTION (RECOMPILE); +IF (@StandbyMode = 1) + BEGIN + IF (@StandbyUndoPath IS NULL) + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. Logs will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END; + ELSE + SET @LogRecoveryOption = N'STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf'''; + END; -/* -This looks for parallel plans -*/ -UPDATE ww -SET ww.is_parallel = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.is_parallel_plan = 1 -OPTION (RECOMPILE); +IF (@LogRecoveryOption = N'') + BEGIN + SET @LogRecoveryOption = N'NORECOVERY'; + END; -/*This looks for old CE*/ +IF (@StopAt IS NOT NULL) +BEGIN -RAISERROR(N'Checking for legacy CE', 0, 1) WITH NOWAIT; + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@StopAt is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; -UPDATE w -SET w.downlevel_estimator = 1 -FROM #working_warnings AS w -JOIN #working_plan_text AS wpt -ON w.plan_id = wpt.plan_id -AND w.query_id = wpt.query_id -/*PLEASE DON'T TELL ANYONE I DID THIS*/ -WHERE PARSENAME(wpt.engine_version, 4) < PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4) -OPTION (RECOMPILE); -/*NO SERIOUSLY THIS IS A HORRIBLE IDEA*/ + IF LEN(@StopAt) <> 14 OR PATINDEX('%[^0-9]%', @StopAt) > 0 + BEGIN + RAISERROR('@StopAt parameter is incorrect. It should contain exactly 14 digits in the format yyyyMMddhhmmss.', 16, 1) WITH NOWAIT; + RETURN + END + IF ISDATE(STUFF(STUFF(STUFF(@StopAt, 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) = 0 + BEGIN + RAISERROR('@StopAt is not a valid datetime.', 16, 1) WITH NOWAIT; + RETURN + END -/*This looks for trivial plans*/ + -- Add the STOPAT parameter to the log recovery options but change the value to a valid DATETIME, e.g. '20211118040230' -> '20211118 04:02:30' + SET @LogRecoveryOption += ', STOPAT = ''' + STUFF(STUFF(STUFF(@StopAt, 13, 0, ':'), 11, 0, ':'), 9, 0, ' ') + '''' -RAISERROR(N'Checking for trivial plans', 0, 1) WITH NOWAIT; + IF @BackupDateTime = @StopAt + BEGIN + IF @Debug = 1 + BEGIN + RAISERROR('@StopAt is the end time of a FULL backup, no log files will be restored.', 0, 1) WITH NOWAIT; + END + END + ELSE + BEGIN + DECLARE @ExtraLogFile NVARCHAR(255) + SELECT TOP 1 @ExtraLogFile = fl.BackupFile + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) > @StopAt + ORDER BY BackupFile; + END -UPDATE w -SET w.is_trivial = 1 -FROM #working_warnings AS w -JOIN #working_plan_text AS wpt -ON w.plan_id = wpt.plan_id -AND w.query_id = wpt.query_id -AND wpt.is_trivial_plan = 1 -OPTION (RECOMPILE); + IF @ExtraLogFile IS NULL + BEGIN + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) > @StopAt; + END + ELSE + BEGIN + -- If this is a split backup, @ExtraLogFile contains only the first split backup file, either _1.trn or _01.trn + -- Change @ExtraLogFile to the max split backup file, then delete all log files greater than this + SET @ExtraLogFile = REPLACE(REPLACE(@ExtraLogFile, '_1.trn', '_9.trn'), '_01.trn', '_64.trn') -/*Plans that compile 2x more than they execute*/ + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND fl.BackupFile > @ExtraLogFile + END +END -RAISERROR(N'Checking for plans that compile 2x more than they execute', 0, 1) WITH NOWAIT; -UPDATE ww -SET ww.is_compile_more = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.count_compiles > (wm.count_executions * 2) -OPTION (RECOMPILE); + -- Group Ordering based on Backup File Name excluding Index {#} to construct coma separated string in "Restore Log" Command +SELECT BackupPath,BackupFile,DENSE_RANK() OVER (ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' )) AS DenseRank INTO #SplitLogBackups +FROM @FileList +WHERE BackupFile IS NOT NULL; -/*Plans that compile 2x more than they execute*/ - -RAISERROR(N'Checking for plans that take more than 5 seconds to bind, compile, or optimize', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_slow_plan = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND (wm.avg_bind_duration > 5000 - OR - wm.avg_compile_duration > 5000 - OR - wm.avg_optimize_duration > 5000 - OR - wm.avg_optimize_cpu_time > 5000) -OPTION (RECOMPILE); +-- Loop through all the files for the database + WHILE 1 = 1 + BEGIN + -- Get the TOP record to use in "Restore HeaderOnly/FileListOnly" statement + SELECT TOP 1 @CurrentBackupPathLog = BackupPath, @BackupFile = BackupFile FROM #SplitLogBackups WHERE DenseRank = @LogRestoreRanking; + IF @@rowcount = 0 BREAK; + IF @i = 1 -/* -This parses the XML from our top plans into smaller chunks for easier consumption -*/ + BEGIN + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathLog + @BackupFile); -RAISERROR(N'Begin XML nodes parsing', 0, 1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @HeadersSQL, @CurrentBackupPathLog, @BackupFile'; + PRINT @sql; + END; -RAISERROR(N'Inserting #statements', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement - FROM #working_warnings AS ww - JOIN #working_plan_text AS wp - ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - CROSS APPLY wp.query_plan_xml.nodes('//p:StmtSimple') AS q(n) -OPTION (RECOMPILE); + EXECUTE (@sql); -RAISERROR(N'Inserting parsed cursor XML to #statements', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement - FROM #working_warnings AS ww - JOIN #working_plan_text AS wp - ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - CROSS APPLY wp.query_plan_xml.nodes('//p:StmtCursor') AS q(n) -OPTION (RECOMPILE); + SELECT TOP 1 @LogFirstLSN = CAST(FirstLSN AS NUMERIC(25, 0)), + @LogLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) + FROM #Headers + WHERE BackupType = 2; -RAISERROR(N'Inserting to #query_plan', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #query_plan WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, query_plan ) -SELECT s.plan_id, s.query_id, s.query_hash, s.sql_handle, q.n.query('.') AS query_plan -FROM #statements AS s - CROSS APPLY s.statement.nodes('//p:QueryPlan') AS q(n) -OPTION (RECOMPILE); + IF (@ContinueLogs = 0 AND @LogFirstLSN <= @FullLastLSN AND @FullLastLSN <= @LogLastLSN AND @RestoreDiff = 0) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 0) + SET @i = 2; -RAISERROR(N'Inserting to #relop', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #relop WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, relop) -SELECT qp.plan_id, qp.query_id, qp.query_hash, qp.sql_handle, q.n.query('.') AS relop -FROM #query_plan qp - CROSS APPLY qp.query_plan.nodes('//p:RelOp') AS q(n) -OPTION (RECOMPILE); + IF (@ContinueLogs = 0 AND @LogFirstLSN <= @DiffLastLSN AND @DiffLastLSN <= @LogLastLSN AND @RestoreDiff = 1) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 1) + SET @i = 2; + DELETE FROM #Headers WHERE BackupType = 2; --- statement level checks -RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.compile_timeout = 1 -FROM #statements s -JOIN #working_warnings AS b -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 -OPTION (RECOMPILE); + END; + IF @i = 1 + BEGIN + IF @Debug = 1 RAISERROR('No Log to Restore', 0, 1) WITH NOWAIT; + END -RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.compile_memory_limit_exceeded = 1 -FROM #statements s -JOIN #working_warnings AS b -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 -OPTION (RECOMPILE); + IF @i = 2 + BEGIN + IF @Execute = 'Y' RAISERROR('@i set to 2, restoring logs', 0, 1) WITH NOWAIT; + IF (SELECT COUNT( * ) FROM #SplitLogBackups WHERE DenseRank = @LogRestoreRanking) > 1 + BEGIN + IF @Debug = 1 RAISERROR ('Split backups found', 0, 1) WITH NOWAIT; + SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitLogBackups + WHERE DenseRank = @LogRestoreRanking + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '' ) + N' WITH ' + @LogRecoveryOption + NCHAR(13) + NCHAR(10); + END; + ELSE + SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathLog + @BackupFile + N''' WITH ' + @LogRecoveryOption + NCHAR(13) + NCHAR(10); -RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -index_dml AS ( - SELECT s.query_hash, - index_dml = CASE WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 - WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 - END - FROM #statements s - ) - UPDATE b - SET b.index_dml = i.index_dml - FROM #working_warnings AS b - JOIN index_dml i - ON i.query_hash = b.query_hash - WHERE i.index_dml = 1 -OPTION (RECOMPILE); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for RESTORE LOG: @RestoreDatabaseName, @CurrentBackupPathLog, @BackupFile'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE LOG', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; -RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -table_dml AS ( - SELECT s.query_hash, - table_dml = CASE WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 - WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 - END - FROM #statements AS s - ) - UPDATE b - SET b.table_dml = t.table_dml - FROM #working_warnings AS b - JOIN table_dml t - ON t.query_hash = b.query_hash - WHERE t.table_dml = 1 -OPTION (RECOMPILE); + SET @LogRestoreRanking += 1; + END; -RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #est_rows (query_hash, estimated_rows) -SELECT DISTINCT - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS query_hash, - c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows -FROM #statements AS s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) -WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; - UPDATE b - SET b.estimated_rows = er.estimated_rows - FROM #working_warnings AS b - JOIN #est_rows er - ON er.query_hash = b.query_hash - OPTION (RECOMPILE); + IF @Debug = 1 + BEGIN + SELECT '#SplitLogBackups' AS table_name, BackupPath, BackupFile FROM #SplitLogBackups; + END +END +-- Put database in a useable state +IF @RunRecovery = 1 + BEGIN + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' WITH RECOVERY'; -/*Begin plan cost calculations*/ -RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #plan_cost WITH (TABLOCK) - ( query_plan_cost, sql_handle, plan_id ) -SELECT DISTINCT - s.statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') query_plan_cost, - s.sql_handle, - s.plan_id -FROM #statements s -OUTER APPLY s.statement.nodes('/p:StmtSimple') AS q(n) -WHERE s.statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 -OPTION (RECOMPILE); + IF @KeepCdc = 1 + SET @sql = @sql + N', KEEP_CDC'; + IF @EnableBroker = 1 + SET @sql = @sql + N', ENABLE_BROKER'; -RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; -WITH pc AS ( - SELECT SUM(DISTINCT pc.query_plan_cost) AS queryplancostsum, pc.sql_handle, pc.plan_id - FROM #plan_cost AS pc - GROUP BY pc.sql_handle, pc.plan_id - ) - UPDATE b - SET b.query_cost = ISNULL(pc.queryplancostsum, 0) - FROM #working_warnings AS b - JOIN pc - ON pc.sql_handle = b.sql_handle - AND pc.plan_id = b.plan_id -OPTION (RECOMPILE); + SET @sql = @sql + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @RestoreDatabaseName'; + PRINT @sql; + END; -/*End plan cost calculations*/ + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RECOVER DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; +-- Ensure simple recovery model +IF @ForceSimpleRecovery = 1 + BEGIN + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + N' SET RECOVERY SIMPLE' + NCHAR(13); -RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.plan_warnings = 1 -FROM #query_plan qp -JOIN #working_warnings b -ON qp.sql_handle = b.sql_handle -AND qp.query_plan.exist('/p:QueryPlan/p:Warnings') = 1 -OPTION (RECOMPILE); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for SET RECOVERY SIMPLE: @RestoreDatabaseName'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'SIMPLE LOGGING', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; -RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.implicit_conversions = 1 -FROM #query_plan qp -JOIN #working_warnings b -ON qp.sql_handle = b.sql_handle -AND qp.query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 -OPTION (RECOMPILE); + -- Run checkdb against this database +IF @RunCheckDB = 1 + BEGIN + SET @sql = N'DBCC CHECKDB (' + @RestoreDatabaseName + N') WITH NO_INFOMSGS, ALL_ERRORMSGS, DATA_PURITY;'; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Run Integrity Check: @RestoreDatabaseName'; + PRINT @sql; + END; -RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, - c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , - c.n.exist('//p:Warnings') AS relop_warnings -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) -) -UPDATE b -SET b.warning_no_join_predicate = x.warning_no_join_predicate, - b.no_stats_warning = x.no_stats_warning, - b.relop_warnings = x.relop_warnings -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'DBCC CHECKDB', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; -RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char -FROM #relop r -CROSS APPLY r.relop.nodes('//p:Object') AS c(n) -) -UPDATE b -SET b.is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -JOIN #working_metrics AS wm -ON b.plan_id = wm.plan_id -AND b.query_id = wm.query_id -AND wm.batch_sql_handle IS NOT NULL -OPTION (RECOMPILE); +IF @DatabaseOwner IS NOT NULL + BEGIN + IF @RunRecovery = 1 + BEGIN + IF EXISTS (SELECT * FROM master.dbo.syslogins WHERE syslogins.loginname = @DatabaseOwner) + BEGIN + SET @sql = N'ALTER AUTHORIZATION ON DATABASE::' + @RestoreDatabaseName + ' TO [' + @DatabaseOwner + ']'; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Set Database Owner'; + PRINT @sql; + END; -RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -) -UPDATE b -SET b.function_count = x.function_count, - b.clr_function_count = x.clr_function_count -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master',@Command = @sql, @CommandType = 'ALTER AUTHORIZATION', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE + BEGIN + PRINT @DatabaseOwner + ' is not a valid Login. Database Owner not set.'; + END + END + ELSE + BEGIN + PRINT @RestoreDatabaseName + ' is still in Recovery, so we are unable to change the database owner to [' + @DatabaseOwner + '].'; + END + END; + IF @SetTrustworthyON = 1 + BEGIN + IF @RunRecovery = 1 + BEGIN + IF IS_SRVROLEMEMBER('sysadmin') = 1 + BEGIN + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + N' SET TRUSTWORTHY ON;'; -RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.key_lookup_cost = x.key_lookup_cost -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost -FROM #relop r -WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for SET TRUSTWORTHY ON'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'ALTER DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE + BEGIN + PRINT 'Current user''s login is NOT a member of the sysadmin role. Database TRUSTWORHY bit has not been enabled.'; + END + END + ELSE + BEGIN + PRINT @RestoreDatabaseName + ' is still in Recovery, so we are unable to enable the TRUSTWORHY bit.'; + END + END; -RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.remote_query_cost = x.remote_query_cost -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost -FROM #relop r -WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); +-- Link a user entry in the sys.database_principals system catalog view in the restored database to a SQL Server login of the same name +IF @FixOrphanUsers = 1 + BEGIN + SET @sql = N' +-- Fixup Orphan Users by setting database user sid to match login sid +DECLARE @FixOrphansSql NVARCHAR(MAX); +DECLARE @OrphanUsers TABLE (SqlToExecute NVARCHAR(MAX)); +USE ' + @RestoreDatabaseName + '; + +INSERT @OrphanUsers +SELECT ''ALTER USER ['' + d.name + ''] WITH LOGIN = ['' + d.name + '']; '' + FROM sys.database_principals d + INNER JOIN master.sys.server_principals s ON d.name COLLATE DATABASE_DEFAULT = s.name COLLATE DATABASE_DEFAULT + WHERE d.type_desc = ''SQL_USER'' + AND d.name NOT IN (''guest'',''dbo'') + AND d.sid <> s.sid + ORDER BY d.name; + +SELECT @FixOrphansSql = (SELECT SqlToExecute AS [text()] FROM @OrphanUsers FOR XML PATH (''''), TYPE).value(''text()[1]'',''NVARCHAR(MAX)''); + +IF @FixOrphansSql IS NULL + PRINT ''No orphan users require a sid fixup.''; +ELSE +BEGIN + PRINT ''Fix Orphan Users: '' + @FixOrphansSql; + EXECUTE(@FixOrphansSql); +END;' + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Fix Orphan Users'; + PRINT @sql; + END; -RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.sort_cost = (x.sort_io + x.sort_cpu) -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - r.relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop r -WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE [dbo].[CommandExecute] @DatabaseContext = 'master', @Command = @sql, @CommandType = 'UPDATE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; +IF @RunStoredProcAfterRestore IS NOT NULL AND LEN(LTRIM(@RunStoredProcAfterRestore)) > 0 +BEGIN + PRINT 'Attempting to run ' + @RunStoredProcAfterRestore + SET @sql = N'EXEC ' + @RestoreDatabaseName + '.' + @RunStoredProcAfterRestore -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM #working_warnings b -JOIN #statements AS s -ON b.sql_handle = s.sql_handle -AND b.is_cursor = 1 -CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n(fn) -OPTION (RECOMPILE); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL when building for @RunStoredProcAfterRestore' + PRINT @sql + END + IF @RunRecovery = 0 + BEGIN + PRINT 'Unable to run Run Stored Procedure After Restore as database is not recovered. Run command again with @RunRecovery = 1' + END + ELSE + BEGIN + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXEC sp_executesql @sql + END +END -RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET -b.is_table_scan = x.is_table_scan, -b.backwards_scan = x.backwards_scan, -b.forced_index = x.forced_index, -b.forced_seek = x.forced_seek, -b.forced_scan = x.forced_scan -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - 0 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop r -CROSS APPLY r.relop.nodes('//p:IndexScan') AS q(n) -UNION ALL -SELECT - r.sql_handle, - 1 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop r -CROSS APPLY r.relop.nodes('//p:TableScan') AS q(n) -) AS x ON b.sql_handle = x.sql_handle -OPTION (RECOMPILE); +-- If test restore then blow the database away (be careful) +IF @TestRestore = 1 + BEGIN + SET @sql = N'DROP DATABASE ' + @RestoreDatabaseName + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE: @RestoreDatabaseName'; + PRINT @sql; + END; -RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_computed_scalar = x.computed_column_function -FROM #working_warnings b -JOIN ( -SELECT r.sql_handle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master',@Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; -RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_computed_filter = x.filter_function -FROM #working_warnings b -JOIN ( -SELECT -r.sql_handle, -c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) -) x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); +-- Clean-Up Tempdb Objects +IF OBJECT_ID( 'tempdb..#SplitFullBackups' ) IS NOT NULL DROP TABLE #SplitFullBackups; +IF OBJECT_ID( 'tempdb..#SplitDiffBackups' ) IS NOT NULL DROP TABLE #SplitDiffBackups; +IF OBJECT_ID( 'tempdb..#SplitLogBackups' ) IS NOT NULL DROP TABLE #SplitLogBackups; +GO +IF OBJECT_ID('dbo.sp_ineachdb') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_ineachdb AS RETURN 0') +GO +ALTER PROCEDURE [dbo].[sp_ineachdb] + -- mssqltips.com/sqlservertip/5694/execute-a-command-in-the-context-of-each-database-in-sql-server--part-2/ + @command nvarchar(max) = NULL, + @replace_character nchar(1) = N'?', + @print_dbname bit = 0, + @select_dbname bit = 0, + @print_command bit = 0, + @print_command_only bit = 0, + @suppress_quotename bit = 0, -- use with caution + @system_only bit = 0, + @user_only bit = 0, + @name_pattern nvarchar(300) = N'%', + @database_list nvarchar(max) = NULL, + @exclude_pattern nvarchar(300) = NULL, + @exclude_list nvarchar(max) = NULL, + @recovery_model_desc nvarchar(120) = NULL, + @compatibility_level tinyint = NULL, + @state_desc nvarchar(120) = N'ONLINE', + @is_read_only bit = 0, + @is_auto_close_on bit = NULL, + @is_auto_shrink_on bit = NULL, + @is_broker_enabled bit = NULL, + @user_access nvarchar(128) = NULL, + @Help bit = 0, + @Version varchar(30) = NULL OUTPUT, + @VersionDate datetime = NULL OUTPUT, + @VersionCheckMode bit = 0, + @is_ag_writeable_copy 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; -RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -IndexOps AS -( - SELECT - r.query_hash, - c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, - c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, - c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, - c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, - c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, - c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, - c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, - c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, - c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, - c.n.exist('@PhysicalOp[.="Table Delete"]') AS td - FROM #relop AS r - CROSS APPLY r.relop.nodes('/p:RelOp') c(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) -), iops AS -( - SELECT ios.query_hash, - SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, - SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, - SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, - SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, - SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, - SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, - SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, - SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, - SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count - FROM IndexOps AS ios - WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', - 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', - 'Table Insert', 'Table Delete', 'Table Update') - GROUP BY ios.query_hash) -UPDATE b -SET b.index_insert_count = iops.index_insert_count, - b.index_update_count = iops.index_update_count, - b.index_delete_count = iops.index_delete_count, - b.cx_insert_count = iops.cx_insert_count, - b.cx_update_count = iops.cx_update_count, - b.cx_delete_count = iops.cx_delete_count, - b.table_insert_count = iops.table_insert_count, - b.table_update_count = iops.table_update_count, - b.table_delete_count = iops.table_delete_count -FROM #working_warnings AS b -JOIN iops ON iops.query_hash = b.query_hash -OPTION (RECOMPILE); + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; + + IF @Help = 1 + BEGIN + + PRINT ' + /* + sp_ineachdb from http://FirstResponderKit.org + + This script will execute a command against multiple databases. + + 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. + + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Tastes awful with marmite. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) + + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + + MIT License + + Copyright (c) Brent Ozar Unlimited + + 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. + + */ + '; + + RETURN -1; + END -RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_spatial = x.is_spatial -FROM #working_warnings AS b -JOIN ( -SELECT r.sql_handle, - 1 AS is_spatial -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp//p:Object') n(fn) -WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); + DECLARE @exec nvarchar(150), + @sx nvarchar(18) = N'.sys.sp_executesql', + @db sysname, + @dbq sysname, + @cmd nvarchar(max), + @thisdb sysname, + @cr char(2) = CHAR(13) + CHAR(10), + @SQLVersion AS tinyint = (@@microsoftversion / 0x1000000) & 0xff, -- Stores the SQL Server Version Number(8(2000),9(2005),10(2008 & 2008R2),11(2012),12(2014),13(2016),14(2017),15(2019) + @ServerName AS sysname = CONVERT(sysname, SERVERPROPERTY('ServerName')), -- Stores the SQL Server Instance name. + @NoSpaces nvarchar(20) = N'%[^' + CHAR(9) + CHAR(32) + CHAR(10) + CHAR(13) + N']%'; --Pattern for PATINDEX -RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_forced_serial = 1 -FROM #query_plan qp -JOIN #working_warnings AS b -ON qp.sql_handle = b.sql_handle -AND b.is_parallel IS NULL -AND qp.query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 -OPTION (RECOMPILE); + CREATE TABLE #ineachdb(id int, name nvarchar(512), is_distributor bit); -RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.columnstore_row_mode = x.is_row_mode -FROM #working_warnings AS b -JOIN ( -SELECT - r.sql_handle, - r.relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode -FROM #relop r -WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); +/* + -- first, let's limit to only DBs the caller is interested in + IF @database_list > N'' + -- comma-separated list of potentially valid/invalid/quoted/unquoted names + BEGIN + ;WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n <= LEN(@database_list)), + names AS + ( + SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@database_list, n, + CHARINDEX(N',', @database_list + N',', n) - n), 1))) + FROM n + WHERE SUBSTRING(N',' + @database_list, n, 1) = N',' + ) + INSERT #ineachdb(id,name,is_distributor) + SELECT d.database_id, d.name, d.is_distributor + FROM sys.databases AS d + WHERE EXISTS (SELECT 1 FROM names WHERE name = d.name) + OPTION (MAXRECURSION 0); + END + ELSE + BEGIN + INSERT #ineachdb(id,name,is_distributor) SELECT database_id, name, is_distributor FROM sys.databases; + END + -- now delete any that have been explicitly excluded - exclude trumps include + IF @exclude_list > N'' + -- comma-separated list of potentially valid/invalid/quoted/unquoted names + BEGIN + ;WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n <= LEN(@exclude_list)), + names AS + ( + SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@exclude_list, n, + CHARINDEX(N',', @exclude_list + N',', n) - n), 1))) + FROM n + WHERE SUBSTRING(N',' + @exclude_list, n, 1) = N',' + ) + DELETE d + FROM #ineachdb AS d + INNER JOIN names + ON names.name = d.name + OPTION (MAXRECURSION 0); + END +*/ -RAISERROR('Checking for row level security only', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_row_level = 1 -FROM #working_warnings b -JOIN #statements s -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 -OPTION (RECOMPILE); +/* +@database_list and @exclude_list are are processed at the same time +1)Read the list searching for a comma or [ +2)If we find a comma, save the name +3)If we find a [, we begin to accumulate the result until we reach closing ], (jumping over escaped ]]). +4)Finally, tabs, line breaks and spaces are removed from unquoted names +*/ +WITH C +AS (SELECT V.SrcList + , CAST('' AS nvarchar(MAX)) AS Name + , V.DBList + , 0 AS InBracket + , 0 AS Quoted + FROM (VALUES ('In', @database_list + ','), ('Out', @exclude_list + ',')) AS V (SrcList, DBList) + UNION ALL + SELECT C.SrcList +-- , IIF(V.Found = '[', '', SUBSTRING(C.DBList, 1, V.Place - 1))/*remove initial [*/ + , CASE WHEN V.Found = '[' THEN '' ELSE SUBSTRING(C.DBList, 1, V.Place - 1) END /*remove initial [*/ + , STUFF(C.DBList, 1, V.Place, '') +-- , IIF(V.Found = '[', 1, 0) + ,Case WHEN V.Found = '[' THEN 1 ELSE 0 END + , 0 + FROM C + CROSS APPLY + ( VALUES (PATINDEX('%[,[]%', C.DBList), SUBSTRING(C.DBList, PATINDEX('%[,[]%', C.DBList), 1))) AS V (Place, Found) + WHERE C.DBList > '' + AND C.InBracket = 0 + UNION ALL + SELECT C.SrcList +-- , CONCAT(C.Name, SUBSTRING(C.DBList, 1, V.Place + W.DoubleBracket - 1)) /*Accumulates only one ] if escaped]] or none if end]*/ + , ISNULL(C.Name,'') + ISNULL(SUBSTRING(C.DBList, 1, V.Place + W.DoubleBracket - 1),'') /*Accumulates only one ] if escaped]] or none if end]*/ + , STUFF(C.DBList, 1, V.Place + W.DoubleBracket, '') + , W.DoubleBracket + , 1 + FROM C + CROSS APPLY (VALUES (CHARINDEX(']', C.DBList))) AS V (Place) + -- CROSS APPLY (VALUES (IIF(SUBSTRING(C.DBList, V.Place + 1, 1) = ']', 1, 0))) AS W (DoubleBracket) + CROSS APPLY (VALUES (CASE WHEN SUBSTRING(C.DBList, V.Place + 1, 1) = ']' THEN 1 ELSE 0 END)) AS W (DoubleBracket) + WHERE C.DBList > '' + AND C.InBracket = 1) + , F +AS (SELECT C.SrcList + , CASE WHEN C.Quoted = 0 THEN + SUBSTRING(C.Name, PATINDEX(@NoSpaces, Name), DATALENGTH (Name)/2 - PATINDEX(@NoSpaces, Name) - PATINDEX(@NoSpaces, REVERSE(Name))+2) + ELSE C.Name END + AS name + FROM C + WHERE C.InBracket = 0 + AND C.Name > '') +INSERT #ineachdb(id,name,is_distributor) +SELECT d.database_id + , d.name + , d.is_distributor +FROM sys.databases AS d +WHERE ( EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'In') + OR @database_list IS NULL) + AND NOT EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'Out') +OPTION (MAXRECURSION 0); +; + -- next, let's delete any that *don't* match various criteria passed in + DELETE dbs FROM #ineachdb AS dbs + WHERE (@system_only = 1 AND (id NOT IN (1,2,3,4) AND is_distributor <> 1)) + OR (@user_only = 1 AND (id IN (1,2,3,4) OR is_distributor = 1)) + OR name NOT LIKE @name_pattern + OR name LIKE @exclude_pattern + OR EXISTS + ( + SELECT 1 + FROM sys.databases AS d + WHERE d.database_id = dbs.id + AND NOT + ( + recovery_model_desc = COALESCE(@recovery_model_desc, recovery_model_desc) + AND compatibility_level = COALESCE(@compatibility_level, compatibility_level) + AND is_read_only = COALESCE(@is_read_only, is_read_only) + AND is_auto_close_on = COALESCE(@is_auto_close_on, is_auto_close_on) + AND is_auto_shrink_on = COALESCE(@is_auto_shrink_on, is_auto_shrink_on) + AND is_broker_enabled = COALESCE(@is_broker_enabled, is_broker_enabled) + ) + ); + + -- 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 + DELETE #ineachdb WHERE + CONVERT(nvarchar(128), DATABASEPROPERTYEX(name, 'UserAccess')) <> @user_access; + END + + -- finally, remove any that are not *fully* online or we can't access + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.databases + WHERE database_id = dbs.id + AND + ( + @state_desc = N'ONLINE' AND + ( + [state] & 992 <> 0 -- inaccessible + OR state_desc <> N'ONLINE' -- not online + OR HAS_DBACCESS(name) = 0 -- don't have access + OR DATABASEPROPERTYEX(name, 'Collation') IS NULL -- not fully online. See "status" here: + -- https://docs.microsoft.com/en-us/sql/t-sql/functions/databasepropertyex-transact-sql + ) + OR (@state_desc <> N'ONLINE' AND state_desc <> @state_desc) + ) + ); -RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( - 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -, selects -AS ( SELECT s.plan_id, s.query_id - FROM #statements AS s - WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) -, spools -AS ( SELECT DISTINCT r.plan_id, - r.query_id, - c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, - c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, - c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds -FROM #relop AS r -JOIN selects AS s -ON s.plan_id = r.plan_id - AND s.query_id = r.query_id -CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) -WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 -) -UPDATE ww - SET ww.index_spool_rows = sp.estimated_rows, - ww.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM #working_warnings ww -JOIN spools sp -ON ww.plan_id = sp.plan_id -AND ww.query_id = sp.query_id -OPTION ( RECOMPILE ); + -- from Andy Mallon / First Responders Kit. Make sure that if we're an + -- AG secondary, we skip any database where allow connections is off + IF @SQLVersion >= 11 AND 3 = (SELECT COUNT(*) FROM sys.all_objects WHERE name IN('availability_replicas','dm_hadr_availability_group_states','dm_hadr_database_replica_states')) + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.dm_hadr_database_replica_states AS drs + INNER JOIN sys.availability_replicas AS ar + ON ar.replica_id = drs.replica_id + INNER JOIN sys.dm_hadr_availability_group_states ags + ON ags.group_id = ar.group_id + WHERE drs.database_id = dbs.id + AND ar.secondary_role_allow_connections = 0 + AND ags.primary_replica <> @ServerName + ); + /* Remove databases which are not the writeable copies in an AG. */ + IF @is_ag_writeable_copy = 1 + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.dm_hadr_database_replica_states AS drs + INNER JOIN sys.availability_replicas AS ar + ON ar.replica_id = drs.replica_id + INNER JOIN sys.dm_hadr_availability_group_states AS ags + ON ags.group_id = ar.group_id + WHERE drs.database_id = dbs.id + AND drs.is_primary_replica <> 1 + AND ags.primary_replica <> @ServerName + ); + END + END + -- Well, if we deleted them all... + IF NOT EXISTS (SELECT 1 FROM #ineachdb) + BEGIN + RAISERROR(N'No databases to process.', 1, 0); + RETURN; + END -IF (PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) >= 14 + -- ok, now, let's go through what we have left + DECLARE dbs CURSOR LOCAL FAST_FORWARD + FOR SELECT DB_NAME(id), QUOTENAME(DB_NAME(id)) + FROM #ineachdb; -BEGIN + OPEN dbs; -RAISERROR(N'Beginning 2017 specfic checks', 0, 1) WITH NOWAIT; + FETCH NEXT FROM dbs INTO @db, @dbq; -RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #stats_agg WITH (TABLOCK) - (sql_handle, last_update, modification_count, sampling_percent, [statistics], [table], [schema], [database]) -SELECT qp.sql_handle, - x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, - x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] -FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) -OPTION (RECOMPILE); + DECLARE @msg1 nvarchar(512) = N'Could not run against %s : %s.', + @msg2 nvarchar(max); + WHILE @@FETCH_STATUS <> -1 + BEGIN + SET @thisdb = CASE WHEN @suppress_quotename = 1 THEN @db ELSE @dbq END; + SET @cmd = REPLACE(@command, @replace_character, REPLACE(@thisdb,'''','''''')); -RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; -WITH stale_stats AS ( - SELECT sa.sql_handle - FROM #stats_agg AS sa - GROUP BY sa.sql_handle - HAVING MAX(sa.last_update) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.modification_count) >= 100000 -) -UPDATE b -SET b.stale_stats = 1 -FROM #working_warnings AS b -JOIN stale_stats os -ON b.sql_handle = os.sql_handle -OPTION (RECOMPILE); + BEGIN TRY + IF @print_dbname = 1 + BEGIN + PRINT N'/* ' + @thisdb + N' */'; + END + IF @select_dbname = 1 + BEGIN + SELECT [ineachdb current database] = @thisdb; + END -RAISERROR(N'Checking for Adaptive Joins', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -aj AS ( - SELECT r.sql_handle - FROM #relop AS r - CROSS APPLY r.relop.nodes('//p:RelOp') x(c) - WHERE x.c.exist('@IsAdaptive[.=1]') = 1 -) -UPDATE b -SET b.is_adaptive = 1 -FROM #working_warnings AS b -JOIN aj -ON b.sql_handle = aj.sql_handle -OPTION (RECOMPILE); + IF 1 IN (@print_command, @print_command_only) + BEGIN + PRINT N'/* For ' + @thisdb + ': */' + @cr + @cr + @cmd + @cr + @cr; + END -END; + IF COALESCE(@print_command_only,0) = 0 + BEGIN + SET @exec = @dbq + @sx; + EXEC @exec @cmd; + END + END TRY + BEGIN CATCH + SET @msg2 = ERROR_MESSAGE(); + RAISERROR(@msg1, 1, 0, @db, @msg2); + END CATCH -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - b.unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') -FROM #query_plan qp -JOIN #working_warnings AS b -ON b.query_hash = qp.query_hash -OPTION (RECOMPILE); + FETCH NEXT FROM dbs INTO @db, @dbq; + END + CLOSE dbs; + DEALLOCATE dbs; +END +GO -RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, tf_pretty AS ( -SELECT qp.sql_handle, - q.n.value('@Value', 'INT') AS trace_flag, - q.n.value('@Scope', 'VARCHAR(10)') AS scope -FROM #query_plan qp -CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) -) -INSERT #trace_flags WITH (TABLOCK) - (sql_handle, global_trace_flags, session_trace_flags ) -SELECT DISTINCT tf1.sql_handle , - STUFF(( - SELECT DISTINCT N', ' + CONVERT(NVARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.sql_handle = tf2.sql_handle - AND tf2.scope = 'Global' - FOR XML PATH(N'')), 1, 2, N'' - ) AS global_trace_flags, - STUFF(( - SELECT DISTINCT N', ' + CONVERT(NVARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.sql_handle = tf2.sql_handle - AND tf2.scope = 'Session' - FOR XML PATH(N'')), 1, 2, N'' - ) AS session_trace_flags -FROM tf_pretty AS tf1 -OPTION (RECOMPILE); +IF (OBJECT_ID('dbo.SqlServerVersions') IS NULL) +BEGIN -UPDATE b -SET b.trace_flags_session = tf.session_trace_flags -FROM #working_warnings AS b -JOIN #trace_flags tf -ON tf.sql_handle = b.sql_handle -OPTION (RECOMPILE); + CREATE TABLE dbo.SqlServerVersions + ( + MajorVersionNumber tinyint not null, + MinorVersionNumber smallint not null, + Branch varchar(34) not null, + [Url] varchar(99) not null, + ReleaseDate date not null, + MainstreamSupportEndDate date not null, + ExtendedSupportEndDate date not null, + MajorVersionName varchar(19) not null, + MinorVersionName varchar(67) not null, + + CONSTRAINT PK_SqlServerVersions PRIMARY KEY CLUSTERED + ( + MajorVersionNumber ASC, + MinorVersionNumber ASC, + ReleaseDate ASC + ) + ); + + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The major version number.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MajorVersionNumber' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The minor version number.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MinorVersionNumber' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The update level of the build. CU indicates a cumulative update. SP indicates a service pack. RTM indicates Release To Manufacturer. GDR indicates a General Distribution Release. QFE indicates Quick Fix Engineering (aka hotfix).' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'Branch' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'A link to the KB article for a version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'Url' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date the version was publicly released.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'ReleaseDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date main stream Microsoft support ends for the version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MainstreamSupportEndDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date extended Microsoft support ends for the version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'ExtendedSupportEndDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The major version name.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MajorVersionName' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The minor version name.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MinorVersionName' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'A reference for SQL Server major and minor versions.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions' -RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -is_paul_white_electric AS ( -SELECT 1 AS [is_paul_white_electric], -r.sql_handle -FROM #relop AS r -CROSS APPLY r.relop.nodes('//p:RelOp') c(n) -WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 -) -UPDATE b -SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM #working_warnings AS b -JOIN is_paul_white_electric ipwe -ON ipwe.sql_handle = b.sql_handle -OPTION (RECOMPILE); +END; +GO -IF EXISTS ( SELECT 1 - FROM #working_warnings AS ww - WHERE ww.implicit_conversions = 1 - OR ww.proc_or_function_name <> N'Statement' ) - BEGIN +DELETE FROM dbo.SqlServerVersions; + +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'), + /*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'), + (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'), + (16, 4140, 'CU14 GDR', 'https://support.microsoft.com/en-us/help/5042578', '2024-09-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 14 GDR'), + (16, 4135, 'CU14', 'https://support.microsoft.com/en-us/help/5038325', '2024-07-23', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 14'), + (16, 4125, 'CU13', 'https://support.microsoft.com/en-us/help/5036432', '2024-05-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 13'), + (16, 4115, 'CU12', 'https://support.microsoft.com/en-us/help/5033663', '2024-03-14', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 12'), + (16, 4105, 'CU11', 'https://support.microsoft.com/en-us/help/5032679', '2024-01-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 11'), + (16, 4100, 'CU10 GDR', 'https://support.microsoft.com/en-us/help/5033592', '2024-01-09', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 10 GDR'), + (16, 4095, 'CU10', 'https://support.microsoft.com/en-us/help/5031778', '2023-11-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 10'), + (16, 4085, 'CU9', 'https://support.microsoft.com/en-us/help/5030731', '2023-10-12', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 9'), + (16, 4075, 'CU8', 'https://support.microsoft.com/en-us/help/5029666', '2023-09-14', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 8'), + (16, 4065, 'CU7', 'https://support.microsoft.com/en-us/help/5028743', '2023-08-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 7'), + (16, 4055, 'CU6', 'https://support.microsoft.com/en-us/help/5027505', '2023-07-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 6'), + (16, 4045, 'CU5', 'https://support.microsoft.com/en-us/help/5026806', '2023-06-15', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 5'), + (16, 4035, 'CU4', 'https://support.microsoft.com/en-us/help/5026717', '2023-05-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 4'), + (16, 4025, 'CU3', 'https://support.microsoft.com/en-us/help/5024396', '2023-04-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 3'), + (16, 4015, 'CU2', 'https://support.microsoft.com/en-us/help/5023127', '2023-03-15', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 2'), + (16, 4003, 'CU1', 'https://support.microsoft.com/en-us/help/5022375', '2023-02-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 1'), + (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'), + (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'), + (15, 4390, 'CU28 GDR', 'https://support.microsoft.com/kb/5042749', '2024-09-10', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 28 GDR'), + (15, 4385, 'CU28', 'https://support.microsoft.com/kb/5039747', '2024-08-01', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 28'), + (15, 4375, 'CU27', 'https://support.microsoft.com/kb/5037331', '2024-06-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 27'), + (15, 4365, 'CU26', 'https://support.microsoft.com/kb/5035123', '2024-04-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 26'), + (15, 4355, 'CU25', 'https://support.microsoft.com/kb/5033688', '2024-02-15', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 25'), + (15, 4345, 'CU24', 'https://support.microsoft.com/kb/5031908', '2023-12-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 24'), + (15, 4335, 'CU23', 'https://support.microsoft.com/kb/5030333', '2023-10-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 23'), + (15, 4322, 'CU22', 'https://support.microsoft.com/kb/5027702', '2023-08-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 22'), + (15, 4316, 'CU21', 'https://support.microsoft.com/kb/5025808', '2023-06-15', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 21'), + (15, 4312, 'CU20', 'https://support.microsoft.com/kb/5024276', '2023-04-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 20'), + (15, 4298, 'CU19', 'https://support.microsoft.com/kb/5023049', '2023-02-16', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 19'), + (15, 4280, 'CU18 GDR', 'https://support.microsoft.com/kb/5021124', '2023-02-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 18 GDR'), + (15, 4261, 'CU18', 'https://support.microsoft.com/en-us/help/5017593', '2022-09-28', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 18'), + (15, 4249, 'CU17', 'https://support.microsoft.com/en-us/help/5016394', '2022-08-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 17'), + (15, 4236, 'CU16 GDR', 'https://support.microsoft.com/en-us/help/5014353', '2022-06-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 16 GDR'), + (15, 4223, 'CU16', 'https://support.microsoft.com/en-us/help/5011644', '2022-04-18', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 16'), + (15, 4198, 'CU15', 'https://support.microsoft.com/en-us/help/5008996', '2022-01-07', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 15'), + (15, 4188, 'CU14', 'https://support.microsoft.com/en-us/help/5007182', '2021-11-22', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 14'), + (15, 4178, 'CU13', 'https://support.microsoft.com/en-us/help/5005679', '2021-10-05', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 13'), + (15, 4153, 'CU12', 'https://support.microsoft.com/en-us/help/5004524', '2021-08-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 12'), + (15, 4138, 'CU11', 'https://support.microsoft.com/en-us/help/5003249', '2021-06-10', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 11'), + (15, 4123, 'CU10', 'https://support.microsoft.com/en-us/help/5001090', '2021-04-06', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 10'), + (15, 4102, 'CU9', 'https://support.microsoft.com/en-us/help/5000642', '2021-02-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 9 '), + (15, 4073, 'CU8 GDR', 'https://support.microsoft.com/en-us/help/4583459', '2021-01-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 8 GDR '), + (15, 4073, 'CU8', 'https://support.microsoft.com/en-us/help/4577194', '2020-10-01', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 8 '), + (15, 4063, 'CU7', 'https://support.microsoft.com/en-us/help/4570012', '2020-09-02', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 7 '), + (15, 4053, 'CU6', 'https://support.microsoft.com/en-us/help/4563110', '2020-08-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 6 '), + (15, 4043, 'CU5', 'https://support.microsoft.com/en-us/help/4548597', '2020-06-22', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 5 '), + (15, 4033, 'CU4', 'https://support.microsoft.com/en-us/help/4548597', '2020-03-31', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 4 '), + (15, 4023, 'CU3', 'https://support.microsoft.com/en-us/help/4538853', '2020-03-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 3 '), + (15, 4013, 'CU2', 'https://support.microsoft.com/en-us/help/4536075', '2020-02-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 2 '), + (15, 4003, 'CU1', 'https://support.microsoft.com/en-us/help/4527376', '2020-01-07', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 1 '), + (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'), + (14, 3471, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5040940', '2024-07-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3465, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5029376', '2023-10-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3460, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5021126', '2023-02-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3456, 'RTM CU31', 'https://support.microsoft.com/en-us/help/5016884', '2022-09-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31'), + (14, 3451, 'RTM CU30', 'https://support.microsoft.com/en-us/help/5013756', '2022-07-13', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 30'), + (14, 3445, 'RTM CU29 GDR', 'https://support.microsoft.com/en-us/help/5014553', '2022-06-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 29 GDR'), + (14, 3436, 'RTM CU29', 'https://support.microsoft.com/en-us/help/5010786', '2022-03-31', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 29'), + (14, 3430, 'RTM CU28', 'https://support.microsoft.com/en-us/help/5006944', '2022-01-13', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 28'), + (14, 3421, 'RTM CU27', 'https://support.microsoft.com/en-us/help/5006944', '2021-10-27', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 27'), + (14, 3411, 'RTM CU26', 'https://support.microsoft.com/en-us/help/5005226', '2021-09-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 26'), + (14, 3401, 'RTM CU25', 'https://support.microsoft.com/en-us/help/5003830', '2021-07-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 25'), + (14, 3391, 'RTM CU24', 'https://support.microsoft.com/en-us/help/5001228', '2021-05-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 24'), + (14, 3381, 'RTM CU23', 'https://support.microsoft.com/en-us/help/5000685', '2021-02-25', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 23'), + (14, 3370, 'RTM CU22 GDR', 'https://support.microsoft.com/en-us/help/4583457', '2021-01-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 22 GDR'), + (14, 3356, 'RTM CU22', 'https://support.microsoft.com/en-us/help/4577467', '2020-09-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 22'), + (14, 3335, 'RTM CU21', 'https://support.microsoft.com/en-us/help/4557397', '2020-07-01', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 21'), + (14, 3294, 'RTM CU20', 'https://support.microsoft.com/en-us/help/4541283', '2020-04-07', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 20'), + (14, 3257, 'RTM CU19', 'https://support.microsoft.com/en-us/help/4535007', '2020-02-05', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 19'), + (14, 3257, 'RTM CU18', 'https://support.microsoft.com/en-us/help/4527377', '2019-12-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 18'), + (14, 3238, 'RTM CU17', 'https://support.microsoft.com/en-us/help/4515579', '2019-10-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 17'), + (14, 3223, 'RTM CU16', 'https://support.microsoft.com/en-us/help/4508218', '2019-08-01', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 16'), + (14, 3162, 'RTM CU15', 'https://support.microsoft.com/en-us/help/4498951', '2019-05-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 15'), + (14, 3076, 'RTM CU14', 'https://support.microsoft.com/en-us/help/4484710', '2019-03-25', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 14'), + (14, 3048, 'RTM CU13', 'https://support.microsoft.com/en-us/help/4466404', '2018-12-18', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 13'), + (14, 3045, 'RTM CU12', 'https://support.microsoft.com/en-us/help/4464082', '2018-10-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 12'), + (14, 3038, 'RTM CU11', 'https://support.microsoft.com/en-us/help/4462262', '2018-09-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 11'), + (14, 3037, 'RTM CU10', 'https://support.microsoft.com/en-us/help/4524334', '2018-08-27', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 10'), + (14, 3030, 'RTM CU9', 'https://support.microsoft.com/en-us/help/4515435', '2018-07-18', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 9'), + (14, 3029, 'RTM CU8', 'https://support.microsoft.com/en-us/help/4338363', '2018-06-21', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 8'), + (14, 3026, 'RTM CU7', 'https://support.microsoft.com/en-us/help/4229789', '2018-05-23', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 7'), + (14, 3025, 'RTM CU6', 'https://support.microsoft.com/en-us/help/4101464', '2018-04-17', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 6'), + (14, 3023, 'RTM CU5', 'https://support.microsoft.com/en-us/help/4092643', '2018-03-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 5'), + (14, 3022, 'RTM CU4', 'https://support.microsoft.com/en-us/help/4056498', '2018-02-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 4'), + (14, 3015, 'RTM CU3', 'https://support.microsoft.com/en-us/help/4052987', '2018-01-04', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 3'), + (14, 3008, 'RTM CU2', 'https://support.microsoft.com/en-us/help/4052574', '2017-11-28', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 2'), + (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'), + (13, 7029, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5029187', '2023-10-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), + (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'), + (13, 6441, 'SP3 GDR', 'https://support.microsoft.com/kb/5040946', '2024-07-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6435, 'SP3 GDR', 'https://support.microsoft.com/kb/5029186', '2023-10-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6430, 'SP3 GDR', 'https://support.microsoft.com/kb/5021129', '2023-02-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6419, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/5014355', '2022-06-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6404, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/5006943', '2021-10-27', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6300, 'SP3 ', 'https://support.microsoft.com/en-us/help/5003279', '2021-09-15', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3'), + (13, 5888, 'SP2 CU17', 'https://support.microsoft.com/en-us/help/5001092', '2021-03-29', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 17'), + (13, 5882, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/5000645', '2021-02-11', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 16'), + (13, 5865, 'SP2 CU15 GDR', 'https://support.microsoft.com/en-us/help/4583461', '2021-01-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 15 GDR'), + (13, 5850, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/4577775', '2020-09-28', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 15'), + (13, 5830, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/4564903', '2020-08-06', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 14'), + (13, 5820, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/4549825', '2020-05-28', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 13'), + (13, 5698, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/4536648', '2020-02-25', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 12'), + (13, 5598, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/4527378', '2019-12-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 11'), + (13, 5492, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/4505830', '2019-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 10'), + (13, 5479, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/4505830', '2019-09-30', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 9'), + (13, 5426, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/4505830', '2019-07-31', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 8'), + (13, 5337, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/4495256', '2019-05-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 7'), + (13, 5292, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/4488536', '2019-03-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 6'), + (13, 5264, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/4475776', '2019-01-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 5'), + (13, 5233, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/4464106', '2018-11-13', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 4'), + (13, 5216, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/4458871', '2018-09-20', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 3'), + (13, 5201, 'SP2 CU2 + Security Update', 'https://support.microsoft.com/en-us/help/4458621', '2018-08-21', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 2 + Security Update'), + (13, 5153, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/4340355', '2018-07-16', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 2'), + (13, 5149, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/4135048', '2018-05-30', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 1'), + (13, 5103, 'SP2 GDR', 'https://support.microsoft.com/en-us/help/4583460', '2021-01-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 GDR'), + (13, 5026, 'SP2 ', 'https://support.microsoft.com/en-us/help/4052908', '2018-04-24', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 '), + (13, 4574, 'SP1 CU15', 'https://support.microsoft.com/en-us/help/4495257', '2019-05-16', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 15'), + (13, 4560, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/4488535', '2019-03-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 14'), + (13, 4550, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/4475775', '2019-01-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 13'), + (13, 4541, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/4464343', '2018-11-13', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 12'), + (13, 4528, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/4459676', '2018-09-17', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 11'), + (13, 4514, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/4341569', '2018-07-16', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 10'), + (13, 4502, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/4100997', '2018-05-30', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 9'), + (13, 4474, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/4077064', '2018-03-19', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 8'), + (13, 4466, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/4057119', '2018-01-04', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 7'), + (13, 4457, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/4037354', '2017-11-20', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 6'), + (13, 4451, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/4024305', '2017-09-18', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 5'), + (13, 4446, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/4024305', '2017-08-08', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 4'), + (13, 4435, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/4019916', '2017-05-15', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 3'), + (13, 4422, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/4013106', '2017-03-20', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 2'), + (13, 4411, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/3208177', '2017-01-17', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 1'), + (13, 4224, 'SP1 CU10 + Security Update', 'https://support.microsoft.com/en-us/help/4458842', '2018-08-22', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 10 + Security Update'), + (13, 4001, 'SP1 ', 'https://support.microsoft.com/en-us/help/3182545 ', '2016-11-16', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 '), + (13, 2216, 'RTM CU9', 'https://support.microsoft.com/en-us/help/4037357', '2017-11-20', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 9'), + (13, 2213, 'RTM CU8', 'https://support.microsoft.com/en-us/help/4024304', '2017-09-18', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 8'), + (13, 2210, 'RTM CU7', 'https://support.microsoft.com/en-us/help/4024304', '2017-08-08', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 7'), + (13, 2204, 'RTM CU6', 'https://support.microsoft.com/en-us/help/4019914', '2017-05-15', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 6'), + (13, 2197, 'RTM CU5', 'https://support.microsoft.com/en-us/help/4013105', '2017-03-20', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 5'), + (13, 2193, 'RTM CU4', 'https://support.microsoft.com/en-us/help/3205052 ', '2017-01-17', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 4'), + (13, 2186, 'RTM CU3', 'https://support.microsoft.com/en-us/help/3205413 ', '2016-11-16', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 3'), + (13, 2164, 'RTM CU2', 'https://support.microsoft.com/en-us/help/3182270 ', '2016-09-22', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 2'), + (13, 2149, 'RTM CU1', 'https://support.microsoft.com/en-us/help/3164674 ', '2016-07-25', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 1'), + (13, 1601, 'RTM ', '', '2016-06-01', '2019-01-09', '2019-01-09', 'SQL Server 2016', 'RTM '), + /*2014*/ + (12, 6449, 'SP3 CU4 GDR', 'https://support.microsoft.com/kb/5029185', '2023-10-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6444, 'SP3 CU4 GDR', 'https://support.microsoft.com/kb/5021045', '2023-02-14', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6439, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/5014164', '2022-06-14', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6433, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/4583462', '2021-01-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6372, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/4535288', '2020-02-11', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6329, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/4500181', '2019-07-29', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4'), + (12, 6259, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/4491539', '2019-04-16', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 3'), + (12, 6214, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/4482960', '2019-02-19', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 2'), + (12, 6205, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/4470220', '2018-12-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 1'), + (12, 6164, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/4583463', '2021-01-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 GDR'), + (12, 6024, 'SP3 ', 'https://support.microsoft.com/en-us/help/4022619', '2018-10-30', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 '), + (12, 5687, 'SP2 CU18', 'https://support.microsoft.com/en-us/help/4500180', '2019-07-29', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 18'), + (12, 5632, 'SP2 CU17', 'https://support.microsoft.com/en-us/help/4491540', '2019-04-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 17'), + (12, 5626, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/4482967', '2019-02-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 16'), + (12, 5605, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/4469137', '2018-12-12', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 15'), + (12, 5600, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/4459860', '2018-10-15', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 14'), + (12, 5590, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/4456287', '2018-08-27', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 13'), + (12, 5589, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/4130489', '2018-06-18', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 12'), + (12, 5579, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/4077063', '2018-03-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 11'), + (12, 5571, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/4052725', '2018-01-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 10'), + (12, 5563, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/4055557', '2017-12-18', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 9'), + (12, 5557, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/4037356', '2017-10-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 8'), + (12, 5556, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/4032541', '2017-08-28', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 7'), + (12, 5553, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/4019094', '2017-08-08', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 6'), + (12, 5546, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/4013098', '2017-04-17', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 5'), + (12, 5540, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/4010394', '2017-02-21', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 4'), + (12, 5538, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/3204388 ', '2016-12-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 3'), + (12, 5522, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/3188778 ', '2016-10-17', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 2'), + (12, 5511, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/3178925 ', '2016-08-25', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 1'), + (12, 5000, 'SP2 ', 'https://support.microsoft.com/en-us/help/3171021 ', '2016-07-11', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 '), + (12, 4522, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/4019099', '2017-08-08', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 13'), + (12, 4511, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/4017793', '2017-04-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 12'), + (12, 4502, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/4010392', '2017-02-21', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 11'), + (12, 4491, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/3204399 ', '2016-12-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 10'), + (12, 4474, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/3186964 ', '2016-10-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 9'), + (12, 4468, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/3174038 ', '2016-08-15', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 8'), + (12, 4459, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/3162659 ', '2016-06-20', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 7'), + (12, 4457, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/3167392 ', '2016-05-30', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 6'), + (12, 4449, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/3144524', '2016-04-18', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 6'), + (12, 4438, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/3130926', '2016-02-22', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 5'), + (12, 4436, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/3106660', '2015-12-21', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 4'), + (12, 4427, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/3094221', '2015-10-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 3'), + (12, 4422, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/3075950', '2015-08-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 2'), + (12, 4416, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/3067839', '2015-06-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 1'), + (12, 4213, 'SP1 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3070446', '2015-07-14', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 MS15-058: GDR Security Update'), + (12, 4100, 'SP1 ', 'https://support.microsoft.com/en-us/help/3058865', '2015-05-04', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 '), + (12, 2569, 'RTM CU14', 'https://support.microsoft.com/en-us/help/3158271 ', '2016-06-20', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 14'), + (12, 2568, 'RTM CU13', 'https://support.microsoft.com/en-us/help/3144517', '2016-04-18', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 13'), + (12, 2564, 'RTM CU12', 'https://support.microsoft.com/en-us/help/3130923', '2016-02-22', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 12'), + (12, 2560, 'RTM CU11', 'https://support.microsoft.com/en-us/help/3106659', '2015-12-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 11'), + (12, 2556, 'RTM CU10', 'https://support.microsoft.com/en-us/help/3094220', '2015-10-19', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 10'), + (12, 2553, 'RTM CU9', 'https://support.microsoft.com/en-us/help/3075949', '2015-08-17', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 9'), + (12, 2548, 'RTM MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045323', '2015-07-14', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS15-058: QFE Security Update'), + (12, 2546, 'RTM CU8', 'https://support.microsoft.com/en-us/help/3067836', '2015-06-19', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 8'), + (12, 2495, 'RTM CU7', 'https://support.microsoft.com/en-us/help/3046038', '2015-04-20', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 7'), + (12, 2480, 'RTM CU6', 'https://support.microsoft.com/en-us/help/3031047', '2015-02-16', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 6'), + (12, 2456, 'RTM CU5', 'https://support.microsoft.com/en-us/help/3011055', '2014-12-17', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 5'), + (12, 2430, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2999197', '2014-10-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 4'), + (12, 2402, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2984923', '2014-08-18', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 3'), + (12, 2381, 'RTM MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2977316', '2014-08-12', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS14-044: QFE Security Update'), + (12, 2370, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2967546', '2014-06-27', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 2'), + (12, 2342, 'RTM CU1', 'https://support.microsoft.com/en-us/help/2931693', '2014-04-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 1'), + (12, 2269, 'RTM MS15-058: GDR Security Update ', 'https://support.microsoft.com/en-us/help/3045324', '2015-07-14', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS15-058: GDR Security Update '), + (12, 2254, 'RTM MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977315', '2014-08-12', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS14-044: GDR Security Update'), + (12, 2000, 'RTM ', '', '2014-04-01', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM '), + /*2012*/ + (11, 7512, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/5021123', '2023-02-16', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2021-1636'), + (11, 7507, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/4583465', '2021-01-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2021-1636'), + (11, 7493, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/4532098', '2020-02-11', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2020-0618'), + (11, 7469, 'SP4 On-Demand Hotfix Update', 'https://support.microsoft.com/en-us/help/4091266', '2018-03-28', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 SP4 On-Demand Hotfix Update'), + (11, 7462, 'SP4 ADV180002: GDR Security Update', 'https://support.microsoft.com/en-us/help/4057116', '2018-01-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 ADV180002: GDR Security Update'), + (11, 7001, 'SP4 ', 'https://support.microsoft.com/en-us/help/4018073', '2017-10-02', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 '), + (11, 6607, 'SP3 CU10', 'https://support.microsoft.com/en-us/help/4025925', '2017-08-08', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 10'), + (11, 6598, 'SP3 CU9', 'https://support.microsoft.com/en-us/help/4016762', '2017-05-15', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 9'), + (11, 6594, 'SP3 CU8', 'https://support.microsoft.com/en-us/help/3205051 ', '2017-03-20', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 8'), + (11, 6579, 'SP3 CU7', 'https://support.microsoft.com/en-us/help/3205051 ', '2017-01-17', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 7'), + (11, 6567, 'SP3 CU6', 'https://support.microsoft.com/en-us/help/3194992 ', '2016-11-17', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 6'), + (11, 6544, 'SP3 CU5', 'https://support.microsoft.com/en-us/help/3180915 ', '2016-09-19', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 5'), + (11, 6540, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/3165264 ', '2016-07-18', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 4'), + (11, 6537, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/3152635 ', '2016-05-16', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 3'), + (11, 6523, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/3137746', '2016-03-21', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 2'), + (11, 6518, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/3123299', '2016-01-19', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 1'), + (11, 6020, 'SP3 ', 'https://support.microsoft.com/en-us/help/3072779', '2015-11-20', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 '), + (11, 5678, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/3205416 ', '2016-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 16'), + (11, 5676, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/3205416 ', '2016-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 15'), + (11, 5657, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/3180914 ', '2016-09-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 14'), + (11, 5655, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/3165266 ', '2016-07-18', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 13'), + (11, 5649, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/3152637 ', '2016-05-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 12'), + (11, 5646, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/3137745', '2016-03-21', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 11'), + (11, 5644, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/3120313', '2016-01-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 10'), + (11, 5641, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/3098512', '2015-11-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 9'), + (11, 5634, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/3082561', '2015-09-21', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 8'), + (11, 5623, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/3072100', '2015-07-20', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 7'), + (11, 5613, 'SP2 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045319', '2015-07-14', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 MS15-058: QFE Security Update'), + (11, 5592, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/3052468', '2015-05-18', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 6'), + (11, 5582, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/3037255', '2015-03-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 5'), + (11, 5569, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/3007556', '2015-01-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 4'), + (11, 5556, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/3002049', '2014-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 3'), + (11, 5548, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2983175', '2014-09-15', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 2'), + (11, 5532, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2976982', '2014-07-23', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 1'), + (11, 5343, 'SP2 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045321', '2015-07-14', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 MS15-058: GDR Security Update'), + (11, 5058, 'SP2 ', 'https://support.microsoft.com/en-us/help/2958429', '2014-06-10', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 '), + (11, 3513, 'SP1 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045317', '2015-07-14', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS15-058: QFE Security Update'), + (11, 3482, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/3002044', '2014-11-17', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 13'), + (11, 3470, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2991533', '2014-09-15', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 12'), + (11, 3460, 'SP1 MS14-044: QFE Security Update ', 'https://support.microsoft.com/en-us/help/2977325', '2014-08-12', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS14-044: QFE Security Update '), + (11, 3449, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2975396', '2014-07-21', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 11'), + (11, 3431, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2954099', '2014-05-19', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 10'), + (11, 3412, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2931078', '2014-03-17', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 9'), + (11, 3401, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/2917531', '2014-01-20', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 8'), + (11, 3393, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/2894115', '2013-11-18', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 7'), + (11, 3381, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/2874879', '2013-09-16', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 6'), + (11, 3373, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/2861107', '2013-07-15', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 5'), + (11, 3368, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/2833645', '2013-05-30', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 4'), + (11, 3349, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/2812412', '2013-03-18', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 3'), + (11, 3339, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/2790947', '2013-01-21', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 2'), + (11, 3321, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/2765331', '2012-11-20', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 1'), + (11, 3156, 'SP1 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045318', '2015-07-14', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS15-058: GDR Security Update'), + (11, 3153, 'SP1 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977326', '2014-08-12', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS14-044: GDR Security Update'), + (11, 3000, 'SP1 ', 'https://support.microsoft.com/en-us/help/2674319', '2012-11-07', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 '), + (11, 2424, 'RTM CU11', 'https://support.microsoft.com/en-us/help/2908007', '2013-12-16', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 11'), + (11, 2420, 'RTM CU10', 'https://support.microsoft.com/en-us/help/2891666', '2013-10-21', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 10'), + (11, 2419, 'RTM CU9', 'https://support.microsoft.com/en-us/help/2867319', '2013-08-20', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 9'), + (11, 2410, 'RTM CU8', 'https://support.microsoft.com/en-us/help/2844205', '2013-06-17', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 8'), + (11, 2405, 'RTM CU7', 'https://support.microsoft.com/en-us/help/2823247', '2013-04-15', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 7'), + (11, 2401, 'RTM CU6', 'https://support.microsoft.com/en-us/help/2728897', '2013-02-18', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 6'), + (11, 2395, 'RTM CU5', 'https://support.microsoft.com/en-us/help/2777772', '2012-12-17', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 5'), + (11, 2383, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2758687', '2012-10-15', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 4'), + (11, 2376, 'RTM MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716441', '2012-10-09', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM MS12-070: QFE Security Update'), + (11, 2332, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2723749', '2012-08-31', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 3'), + (11, 2325, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2703275', '2012-06-18', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 2'), + (11, 2316, 'RTM CU1', 'https://support.microsoft.com/en-us/help/2679368', '2012-04-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 1'), + (11, 2218, 'RTM MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716442', '2012-10-09', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM MS12-070: GDR Security Update'), + (11, 2100, 'RTM ', '', '2012-03-06', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM '), + /*2008 R2*/ + (10, 6529, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045314', '2015-07-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6220, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045316', '2015-07-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6000, 'SP3 ', 'https://support.microsoft.com/en-us/help/2979597', '2014-09-26', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 '), + (10, 4339, 'SP2 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045312', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS15-058: QFE Security Update'), + (10, 4321, 'SP2 MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2977319', '2014-08-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS14-044: QFE Security Update'), + (10, 4319, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/2967540', '2014-06-30', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 13'), + (10, 4305, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/2938478', '2014-04-21', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 12'), + (10, 4302, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/2926028', '2014-02-18', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 11'), + (10, 4297, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/2908087', '2013-12-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 10'), + (10, 4295, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/2887606', '2013-10-28', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 9'), + (10, 4290, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/2871401', '2013-08-22', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 8'), + (10, 4285, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/2844090', '2013-06-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 7'), + (10, 4279, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/2830140', '2013-04-15', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 6'), + (10, 4276, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/2797460', '2013-02-18', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 5'), + (10, 4270, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/2777358', '2012-12-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 4'), + (10, 4266, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/2754552', '2012-10-15', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 3'), + (10, 4263, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2740411', '2012-08-31', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 2'), + (10, 4260, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2720425', '2012-07-24', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 1'), + (10, 4042, 'SP2 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045313', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS15-058: GDR Security Update'), + (10, 4033, 'SP2 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977320', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS14-044: GDR Security Update'), + (10, 4000, 'SP2 ', 'https://support.microsoft.com/en-us/help/2630458', '2012-07-26', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 '), + (10, 2881, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/2868244', '2013-08-08', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 14'), + (10, 2876, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/2855792', '2013-06-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 13'), + (10, 2874, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2828727', '2013-04-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 12'), + (10, 2869, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2812683', '2013-02-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 11'), + (10, 2868, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2783135', '2012-12-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 10'), + (10, 2866, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2756574', '2012-10-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 9'), + (10, 2861, 'SP1 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716439', '2012-10-09', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 MS12-070: QFE Security Update'), + (10, 2822, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/2723743', '2012-08-31', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 8'), + (10, 2817, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/2703282', '2012-06-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 7'), + (10, 2811, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/2679367', '2012-04-16', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 6'), + (10, 2806, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/2659694', '2012-02-22', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 5'), + (10, 2796, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/2633146', '2011-12-19', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 4'), + (10, 2789, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/2591748', '2011-10-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 3'), + (10, 2772, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/2567714', '2011-08-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 2'), + (10, 2769, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/2544793', '2011-07-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 1'), + (10, 2550, 'SP1 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2754849', '2012-10-09', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 MS12-070: GDR Security Update'), + (10, 2500, 'SP1 ', 'https://support.microsoft.com/en-us/help/2528583', '2011-07-12', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 '), + (10, 1815, 'RTM CU13', 'https://support.microsoft.com/en-us/help/2679366', '2012-04-16', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 13'), + (10, 1810, 'RTM CU12', 'https://support.microsoft.com/en-us/help/2659692', '2012-02-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 12'), + (10, 1809, 'RTM CU11', 'https://support.microsoft.com/en-us/help/2633145', '2011-12-19', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 11'), + (10, 1807, 'RTM CU10', 'https://support.microsoft.com/en-us/help/2591746', '2011-10-17', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 10'), + (10, 1804, 'RTM CU9', 'https://support.microsoft.com/en-us/help/2567713', '2011-08-15', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 9'), + (10, 1797, 'RTM CU8', 'https://support.microsoft.com/en-us/help/2534352', '2011-06-20', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 8'), + (10, 1790, 'RTM MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494086', '2011-06-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM MS11-049: QFE Security Update'), + (10, 1777, 'RTM CU7', 'https://support.microsoft.com/en-us/help/2507770', '2011-04-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 7'), + (10, 1765, 'RTM CU6', 'https://support.microsoft.com/en-us/help/2489376', '2011-02-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 6'), + (10, 1753, 'RTM CU5', 'https://support.microsoft.com/en-us/help/2438347', '2010-12-20', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 5'), + (10, 1746, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2345451', '2010-10-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 4'), + (10, 1734, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2261464', '2010-08-16', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 3'), + (10, 1720, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2072493', '2010-06-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 2'), + (10, 1702, 'RTM CU1', 'https://support.microsoft.com/en-us/help/981355', '2010-05-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 1'), + (10, 1617, 'RTM MS11-049: GDR Security Update', 'https://support.microsoft.com/en-us/help/2494088', '2011-06-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM MS11-049: GDR Security Update'), + (10, 1600, 'RTM ', '', '2010-05-10', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM '), + /*2008*/ + (10, 6535, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045308', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6241, 'SP3 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045311', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: GDR Security Update'), + (10, 5890, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045303', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 5869, 'SP3 MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2984340, https://support.microsoft.com/en-us/help/2977322', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS14-044: QFE Security Update'), + (10, 5861, 'SP3 CU17', 'https://support.microsoft.com/en-us/help/2958696', '2014-05-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 17'), + (10, 5852, 'SP3 CU16', 'https://support.microsoft.com/en-us/help/2936421', '2014-03-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 16'), + (10, 5850, 'SP3 CU15', 'https://support.microsoft.com/en-us/help/2923520', '2014-01-20', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 15'), + (10, 5848, 'SP3 CU14', 'https://support.microsoft.com/en-us/help/2893410', '2013-11-18', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 14'), + (10, 5846, 'SP3 CU13', 'https://support.microsoft.com/en-us/help/2880350', '2013-09-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 13'), + (10, 5844, 'SP3 CU12', 'https://support.microsoft.com/en-us/help/2863205', '2013-07-15', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 12'), + (10, 5840, 'SP3 CU11', 'https://support.microsoft.com/en-us/help/2834048', '2013-05-20', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 11'), + (10, 5835, 'SP3 CU10', 'https://support.microsoft.com/en-us/help/2814783', '2013-03-18', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 10'), + (10, 5829, 'SP3 CU9', 'https://support.microsoft.com/en-us/help/2799883', '2013-01-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 9'), + (10, 5828, 'SP3 CU8', 'https://support.microsoft.com/en-us/help/2771833', '2012-11-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 8'), + (10, 5826, 'SP3 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716435', '2012-10-09', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS12-070: QFE Security Update'), + (10, 5794, 'SP3 CU7', 'https://support.microsoft.com/en-us/help/2738350', '2012-09-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 7'), + (10, 5788, 'SP3 CU6', 'https://support.microsoft.com/en-us/help/2715953', '2012-07-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 6'), + (10, 5785, 'SP3 CU5', 'https://support.microsoft.com/en-us/help/2696626', '2012-05-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 5'), + (10, 5775, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/2673383', '2012-03-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 4'), + (10, 5770, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/2648098', '2012-01-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 3'), + (10, 5768, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/2633143', '2011-11-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 2'), + (10, 5766, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/2617146', '2011-10-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 1'), + (10, 5538, 'SP3 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045305', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: GDR Security Update'), + (10, 5520, 'SP3 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977321', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS14-044: GDR Security Update'), + (10, 5512, 'SP3 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716436', '2012-10-09', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS12-070: GDR Security Update'), + (10, 5500, 'SP3 ', 'https://support.microsoft.com/en-us/help/2546951', '2011-10-06', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 '), + (10, 4371, 'SP2 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716433', '2012-10-09', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS12-070: QFE Security Update'), + (10, 4333, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/2715951', '2012-07-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 11'), + (10, 4332, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/2696625', '2012-05-21', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 10'), + (10, 4330, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/2673382', '2012-03-19', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 9'), + (10, 4326, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/2648096', '2012-01-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 8'), + (10, 4323, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/2617148', '2011-11-21', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 7'), + (10, 4321, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/2582285', '2011-09-19', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 6'), + (10, 4316, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/2555408', '2011-07-18', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 5'), + (10, 4311, 'SP2 MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494094', '2011-06-14', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS11-049: QFE Security Update'), + (10, 4285, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/2527180', '2011-05-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 4'), + (10, 4279, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/2498535', '2011-03-17', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 3'), + (10, 4272, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2467239', '2011-01-17', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 2'), + (10, 4266, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2289254', '2010-11-15', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 1'), + (10, 4067, 'SP2 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716434', '2012-10-09', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS12-070: GDR Security Update'), + (10, 4064, 'SP2 MS11-049: GDR Security Update', 'https://support.microsoft.com/en-us/help/2494089', '2011-06-14', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS11-049: GDR Security Update'), + (10, 4000, 'SP2 ', 'https://support.microsoft.com/en-us/help/2285068', '2010-09-29', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 '), + (10, 2850, 'SP1 CU16', 'https://support.microsoft.com/en-us/help/2582282', '2011-09-19', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 16'), + (10, 2847, 'SP1 CU15', 'https://support.microsoft.com/en-us/help/2555406', '2011-07-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 15'), + (10, 2841, 'SP1 MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494100', '2011-06-14', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 MS11-049: QFE Security Update'), + (10, 2821, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/2527187', '2011-05-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 14'), + (10, 2816, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/2497673', '2011-03-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 13'), + (10, 2808, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2467236', '2011-01-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 12'), + (10, 2804, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2413738', '2010-11-15', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 11'), + (10, 2799, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2279604', '2010-09-20', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 10'), + (10, 2789, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2083921', '2010-07-19', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 9'), + (10, 2775, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/981702', '2010-05-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 8'), + (10, 2766, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/979065', '2010-03-26', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 7'), + (10, 2757, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/977443', '2010-01-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 6'), + (10, 2746, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/975977', '2009-11-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 5'), + (10, 2734, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/973602', '2009-09-21', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 4'), + (10, 2723, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/971491', '2009-07-20', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 3'), + (10, 2714, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/970315', '2009-05-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 2'), + (10, 2710, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/969099', '2009-04-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 1'), + (10, 2573, 'SP1 MS11-049: GDR Security update', 'https://support.microsoft.com/en-us/help/2494096', '2011-06-14', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 MS11-049: GDR Security update'), + (10, 2531, 'SP1 ', '', '2009-04-01', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 '), + (10, 1835, 'RTM CU10', 'https://support.microsoft.com/en-us/help/979064', '2010-03-15', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 10'), + (10, 1828, 'RTM CU9', 'https://support.microsoft.com/en-us/help/977444', '2010-01-18', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 9'), + (10, 1823, 'RTM CU8', 'https://support.microsoft.com/en-us/help/975976', '2009-11-16', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 8'), + (10, 1818, 'RTM CU7', 'https://support.microsoft.com/en-us/help/973601', '2009-09-21', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 7'), + (10, 1812, 'RTM CU6', 'https://support.microsoft.com/en-us/help/971490', '2009-07-20', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 6'), + (10, 1806, 'RTM CU5', 'https://support.microsoft.com/en-us/help/969531', '2009-05-18', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 5'), + (10, 1798, 'RTM CU4', 'https://support.microsoft.com/en-us/help/963036', '2009-03-16', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 4'), + (10, 1787, 'RTM CU3', 'https://support.microsoft.com/en-us/help/960484', '2009-01-19', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 3'), + (10, 1779, 'RTM CU2', 'https://support.microsoft.com/en-us/help/958186', '2008-11-19', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 2'), + (10, 1763, 'RTM CU1', 'https://support.microsoft.com/en-us/help/956717', '2008-09-22', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 1'), + (10, 1600, 'RTM ', '', '2008-08-06', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM ') +; +GO +IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;'); +GO - RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; - RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #variable_info ( query_hash, sql_handle, proc_name, variable_name, variable_datatype, compile_time_value ) - SELECT DISTINCT - qp.query_hash, - qp.sql_handle, - b.proc_or_function_name AS proc_name, - q.n.value('@Column', 'NVARCHAR(128)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(128)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(1000)') AS compile_time_value - FROM #query_plan AS qp - JOIN #working_warnings AS b - ON (b.query_hash = qp.query_hash AND b.proc_or_function_name = 'adhoc') - OR (b.sql_handle = qp.sql_handle AND b.proc_or_function_name <> 'adhoc') - CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) - OPTION ( RECOMPILE ); +ALTER PROCEDURE [dbo].[sp_BlitzFirst] + @LogMessage NVARCHAR(4000) = NULL , + @Help TINYINT = 0 , + @AsOf DATETIMEOFFSET = NULL , + @ExpertMode TINYINT = 0 , + @Seconds INT = 5 , + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(256) = NULL , + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputTableNameFileStats NVARCHAR(256) = NULL , + @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , + @OutputTableNameWaitStats NVARCHAR(256) = NULL , + @OutputTableNameBlitzCache NVARCHAR(256) = NULL , + @OutputTableNameBlitzWho NVARCHAR(256) = NULL , + @OutputResultSets NVARCHAR(500) = N'BlitzWho_Start|Findings|FileStats|PerfmonStats|WaitStats|BlitzCache|BlitzWho_End' , + @OutputTableRetentionDays TINYINT = 7 , + @OutputXMLasNVARCHAR TINYINT = 0 , + @FilterPlansByDatabase VARCHAR(MAX) = NULL , + @CheckProcedureCache TINYINT = 0 , + @CheckServerInfo TINYINT = 1 , + @FileLatencyThresholdMS INT = 100 , + @SinceStartup TINYINT = 0 , + @ShowSleepingSPIDs TINYINT = 0 , + @BlitzCacheSkipAnalysis BIT = 1 , + @MemoryGrantThresholdPct DECIMAL(5,2) = 15.00, + @LogMessageCheckID INT = 38, + @LogMessagePriority TINYINT = 1, + @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', + @LogMessageFinding VARCHAR(200) = 'Logged from sp_BlitzFirst', + @LogMessageURL VARCHAR(200) = '', + @LogMessageCheckDate DATETIMEOFFSET = NULL, + @Debug BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 + WITH EXECUTE AS CALLER, RECOMPILE +AS +BEGIN +SET NOCOUNT ON; +SET STATISTICS XML OFF; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +SELECT @Version = '8.26', @VersionDate = '20251002'; - RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #conversion_info ( query_hash, sql_handle, proc_name, expression ) - SELECT DISTINCT - qp.query_hash, - qp.sql_handle, - b.proc_or_function_name AS proc_name, - qq.c.value('@Expression', 'NVARCHAR(128)') AS expression - FROM #query_plan AS qp - JOIN #working_warnings AS b - ON (b.query_hash = qp.query_hash AND b.proc_or_function_name = 'adhoc') - OR (b.sql_handle = qp.sql_handle AND b.proc_or_function_name <> 'adhoc') - CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) - WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 - AND b.implicit_conversions = 1 - OPTION ( RECOMPILE ); +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; - RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( sql_handle, query_hash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) - SELECT ci.sql_handle, - ci.query_hash, - ci.proc_name, - CASE WHEN ci.at_charindex > 0 - AND ci.bracket_charindex > 0 - THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) - ELSE N'**no_variable**' - END AS variable_name, - N'**no_variable**' AS variable_datatype, - CASE WHEN ci.at_charindex = 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column**' - END AS converted_column_name, - CASE WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex = 0 - THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) - WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex > 0 - THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) - WHEN ci.at_charindex > 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column **' - END AS column_name, - CASE WHEN ci.paren_charindex > 0 - 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 - 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 - FROM #conversion_info AS ci - OPTION ( RECOMPILE ); +IF @Help = 1 +BEGIN +PRINT ' +sp_BlitzFirst from http://FirstResponderKit.org + +This script gives you a prioritized list of why your SQL Server is slow right now. - IF EXISTS ( SELECT * - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) - OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) - AND sp.variable_name = vi.variable_name ) - BEGIN - RAISERROR(N'Updating variables', 0, 1) WITH NOWAIT; - UPDATE sp - SET sp.variable_datatype = vi.variable_datatype, - sp.compile_time_value = vi.compile_time_value - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) - OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) - AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); - END - ELSE - BEGIN - RAISERROR(N'Inserting variables', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( sql_handle, query_hash, variable_name, variable_datatype, compile_time_value, proc_name ) - SELECT vi.sql_handle, vi.query_hash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, vi.proc_name - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - END - - RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; - UPDATE s - SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) - ELSE s.variable_datatype - END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) - ELSE s.converted_to - END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) - WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' THEN - QUOTENAME(compile_time_value, '''') - ELSE s.compile_time_value - END - FROM #stored_proc_info AS s - OPTION(RECOMPILE); +This is not an overall health check - for that, check out sp_Blitz. - RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; - WITH precheck AS ( - SELECT spi.sql_handle, - spi.proc_name, - CONVERT(XML, - N' 'Statement' - THEN N'The stored procedure ' + spi.proc_name - ELSE N'This ad hoc statement' - END - + N' had the following implicit conversions: ' - + CHAR(10) - + STUFF(( - SELECT DISTINCT - @cr + @lf - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN N'The variable ' - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'The compiled value ' - WHEN spi2.column_name LIKE '%Expr%' - THEN 'The expression ' - ELSE N'The column ' - END - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN spi2.variable_name - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN spi2.compile_time_value - - ELSE spi2.column_name - END - + N' has a data type of ' - + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to - ELSE spi2.variable_datatype - END - + N' which caused implicit conversion on the column ' - + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' - THEN spi2.converted_column_name - WHEN spi2.column_name = N'**no_column**' - THEN spi2.converted_column_name - WHEN spi2.converted_column_name = N'**no_column**' - THEN spi2.column_name - WHEN spi2.column_name <> spi2.converted_column_name - THEN spi2.converted_column_name - ELSE spi2.column_name - END - + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'' - WHEN spi2.column_name LIKE '%Expr%' - THEN N'' - WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') - THEN ' with the value ' + RTRIM(spi2.compile_time_value) - ELSE N'' - END - + '.' - FROM #stored_proc_info AS spi2 - WHERE spi.sql_handle = spi2.sql_handle - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info - FROM #stored_proc_info AS spi - GROUP BY spi.sql_handle, spi.proc_name - ) - UPDATE b - SET b.implicit_conversion_info = pk.implicit_conversion_info - FROM #working_warnings AS b - JOIN precheck AS pk - ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); - - RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; - WITH precheck AS ( - SELECT spi.sql_handle, - spi.proc_name, - CONVERT(XML, - N' N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' - THEN spi2.variable_name + N' = ' - ELSE @cr + @lf + N' We could not find any cached parameter values for this stored proc. ' - END - + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' - THEN @cr + @lf + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' - WHEN spi2.compile_time_value = N'NULL' - THEN spi2.compile_time_value - ELSE RTRIM(spi2.compile_time_value) - END - FROM #stored_proc_info AS spi2 - WHERE spi.sql_handle = spi2.sql_handle - AND spi2.proc_name <> N'Statement' - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @cr + @lf - + N' -- ?>' - ) AS cached_execution_parameters - FROM #stored_proc_info AS spi - GROUP BY spi.sql_handle, spi.proc_name - ) - UPDATE b - SET b.cached_execution_parameters = pk.cached_execution_parameters - FROM #working_warnings AS b - JOIN precheck AS pk - ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); - - - END; --End implicit conversion information gathering +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. +Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. It + may work just fine on 2005, and if it does, hug your parents. Just don''t + file support issues if it breaks. + - If a temp table called #CustomPerfmonCounters exists for any other session, + but not our session, this stored proc will fail with an error saying the + temp table #CustomPerfmonCounters does not exist. + - @OutputServerName is not functional yet. + - If @OutputDatabaseName, SchemaName, TableName, etc are quoted with brackets, + the write to table may silently fail. Look, I never said I was good at this. -UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN - N'' - ELSE b.implicit_conversion_info - END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN - N'' - ELSE b.cached_execution_parameters - END -FROM #working_warnings AS b -OPTION ( RECOMPILE ); +Unknown limitations of this version: + - None. Like Zombo.com, the only limit is yourself. -/*Begin Missing Index*/ +Changes - for the full list of improvements and fixes in this version, see: +https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ -IF EXISTS - (SELECT 1 FROM #working_warnings AS ww WHERE ww.missing_index_count > 0 ) - - BEGIN - - RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_xml - SELECT qp.query_hash, - qp.sql_handle, - c.mg.value('@Impact', 'FLOAT') AS Impact, - c.mg.query('.') AS cmg - FROM #query_plan AS qp - CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) - WHERE qp.query_hash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); - RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_schema - SELECT mix.query_hash, mix.sql_handle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , - c.mi.query('.') - FROM #missing_index_xml AS mix - CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_usage - SELECT ms.query_hash, ms.sql_handle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, - c.cg.value('@Usage', 'NVARCHAR(128)'), - c.cg.query('.') - FROM #missing_index_schema ms - CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_detail - SELECT miu.query_hash, - miu.sql_handle, - miu.impact, - miu.database_name, - miu.schema_name, - miu.table_name, - miu.usage, - c.c.value('@Name', 'NVARCHAR(128)') - FROM #missing_index_usage AS miu - CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_pretty', 0, 1) WITH NOWAIT; - INSERT #missing_index_pretty - SELECT m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'EQUALITY' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INEQUALITY' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INCLUDE' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] - FROM #missing_index_detail AS m - GROUP BY m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); - - RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; - WITH missing AS ( - SELECT mip.query_hash, - mip.sql_handle, - CONVERT(XML, - N'' - ) AS full_details - FROM #missing_index_pretty AS mip - GROUP BY mip.query_hash, mip.sql_handle, mip.impact - ) - UPDATE ww - SET ww.missing_indexes = m.full_details - FROM #working_warnings AS ww - JOIN missing AS m - ON m.sql_handle = ww.sql_handle - OPTION(RECOMPILE); +MIT License - - END +Copyright (c) Brent Ozar Unlimited - RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; - UPDATE ww - SET ww.missing_indexes = - CASE WHEN ww.missing_indexes IS NULL - THEN '' - ELSE ww.missing_indexes - END - FROM #working_warnings AS ww - OPTION(RECOMPILE); +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: -/*End Missing Index*/ +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. -RAISERROR(N'General query dispositions: frequent executions, long running, etc.', 0, 1) WITH NOWAIT; +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. -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.frequent_execution = CASE WHEN wm.xpm > @execution_threshold THEN 1 END , - b.near_parallel = CASE WHEN b.query_cost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, - b.long_running = CASE WHEN wm.avg_duration > @long_running_query_warning_seconds THEN 1 - WHEN wm.max_duration > @long_running_query_warning_seconds THEN 1 - WHEN wm.avg_cpu_time > @long_running_query_warning_seconds THEN 1 - WHEN wm.max_cpu_time > @long_running_query_warning_seconds THEN 1 END, - b.is_key_lookup_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.key_lookup_cost >= b.query_cost * .5 THEN 1 END, - b.is_sort_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.sort_cost >= b.query_cost * .5 THEN 1 END, - b.is_remote_query_expensive = CASE WHEN b.remote_query_cost >= b.query_cost * .05 THEN 1 END, - b.is_unused_grant = CASE WHEN percent_memory_grant_used <= @memory_grant_warning_percent AND min_grant_kb > @min_memory_per_query THEN 1 END, - b.long_running_low_cpu = CASE WHEN wm.avg_duration > wm.avg_cpu_time * 4 THEN 1 END, - b.low_cost_high_cpu = CASE WHEN b.query_cost < @ctp AND wm.avg_cpu_time > 500. AND b.query_cost * 10 < wm.avg_cpu_time THEN 1 END, - b.is_spool_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.index_spool_cost >= b.query_cost * .1 THEN 1 END, - b.is_spool_more_rows = CASE WHEN b.index_spool_rows >= wm.min_rowcount THEN 1 END, - b.is_bad_estimate = CASE WHEN wm.avg_rowcount > 0 AND (b.estimated_rows * 10000 < wm.avg_rowcount OR b.estimated_rows > wm.avg_rowcount * 10000) THEN 1 END, - b.is_big_log = CASE WHEN wm.avg_log_bytes_used >= (@log_size_mb / 2.) THEN 1 END, - b.is_big_tempdb = CASE WHEN wm.avg_tempdb_space_used >= (@avg_tempdb_data_file / 2.) THEN 1 END -FROM #working_warnings AS b -JOIN #working_metrics AS wm -ON b.plan_id = wm.plan_id -AND b.query_id = wm.query_id -JOIN #working_plan_text AS wpt -ON b.plan_id = wpt.plan_id -AND b.query_id = wpt.query_id -OPTION (RECOMPILE); +'; +RETURN; +END; /* @Help = 1 */ +RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; +DECLARE @StringToExecute NVARCHAR(MAX), + @ParmDefinitions NVARCHAR(4000), + @Parm1 NVARCHAR(4000), + @OurSessionID INT, + @LineFeed NVARCHAR(10), + @StockWarningHeader NVARCHAR(MAX) = N'', + @StockWarningFooter NVARCHAR(MAX) = N'', + @StockDetailsHeader NVARCHAR(MAX) = N'', + @StockDetailsFooter NVARCHAR(MAX) = N'', + @StartSampleTime DATETIMEOFFSET, + @FinishSampleTime DATETIMEOFFSET, + @FinishSampleTimeWaitFor DATETIME, + @AsOf1 DATETIMEOFFSET, + @AsOf2 DATETIMEOFFSET, + @ServiceName sysname, + @OutputTableNameFileStats_View NVARCHAR(256), + @OutputTableNamePerfmonStats_View NVARCHAR(256), + @OutputTableNamePerfmonStatsActuals_View NVARCHAR(256), + @OutputTableNameWaitStats_View NVARCHAR(256), + @OutputTableNameWaitStats_Categories NVARCHAR(256), + @OutputTableCleanupDate DATE, + @ObjectFullName NVARCHAR(2000), + @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', + @BlitzCacheMinutesBack INT, + @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , + @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , + @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName , + @LocalServerName NVARCHAR(128) = CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), + @dm_exec_query_statistics_xml BIT = 0, + @total_cpu_usage BIT = 0, + @get_thread_time_ms NVARCHAR(MAX) = N'', + @thread_time_ms FLOAT = 0, + @logical_processors INT = 0, + @max_worker_threads INT = 0; -RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; -/* Populate warnings */ -UPDATE b -SET b.warnings = SUBSTRING( - CASE WHEN b.warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN b.compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN b.compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN b.is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN b.is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - CASE WHEN b.unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN b.missing_index_count > 0 THEN ', Missing Indexes (' + CAST(b.missing_index_count AS NVARCHAR(3)) + ')' ELSE '' END + - CASE WHEN b.unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(b.unmatched_index_count AS NVARCHAR(3)) + ')' ELSE '' END + - CASE WHEN b.is_cursor = 1 THEN ', Cursor' - + CASE WHEN b.is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN b.is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN b.is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN b.near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN b.frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN b.plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN b.parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN b.long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN b.downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN b.implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN b.plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN b.is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN b.is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN b.is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN b.is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN b.trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + b.trace_flags_session ELSE '' END + - CASE WHEN b.is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN b.function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), b.function_count) + ' function(s)' ELSE '' END + - CASE WHEN b.clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), b.clr_function_count) + ' CLR function(s)' ELSE '' END + - CASE WHEN b.is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN b.no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN b.relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN b.is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN b.backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN b.forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN b.forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN b.forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN b.columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN b.is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN b.is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN b.is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN b.index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN b.is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN b.is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN b.index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN b.table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN b.low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN b.long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN b.stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN b.is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN b.is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN b.is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN b.is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN b.is_big_log = 1 THEN + ', High log use' ELSE '' END + - CASE WHEN b.is_big_tempdb = 1 THEN ', High tempdb use' ELSE '' END + - CASE WHEN b.is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM #working_warnings b -OPTION (RECOMPILE); +/* Sanitize our inputs */ +SELECT + @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), + @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), + @OutputTableNamePerfmonStatsActuals_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Actuals'), + @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), + @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); +SELECT + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName), + @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), + @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), + @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), + @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE), + /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ + /* @OutputTableNameBlitzWho = QUOTENAME(@OutputTableNameBlitzWho), We purposely don't sanitize this because sp_BlitzWho will */ + @LineFeed = CHAR(13) + CHAR(10), + @OurSessionID = @@SPID, + @OutputType = UPPER(@OutputType); +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure generating warnings.', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; +IF UPPER(@OutputType) LIKE 'TOP 10%' SET @OutputType = 'Top10'; +IF @OutputType = 'Top10' SET @SinceStartup = 1; - RETURN; -END CATCH; +/* Logged Message - CheckID 38 */ +IF @LogMessage IS NOT NULL + BEGIN + RAISERROR('Saving LogMessage to table',10,1) WITH NOWAIT; -BEGIN TRY -BEGIN + /* Try to set the output table parameters if they don't exist */ + IF @OutputSchemaName IS NULL AND @OutputTableName IS NULL AND @OutputDatabaseName IS NULL + BEGIN + SET @OutputSchemaName = N'[dbo]'; + SET @OutputTableName = N'[BlitzFirst]'; -RAISERROR(N'Checking for parameter sniffing symptoms', 0, 1) WITH NOWAIT; + /* Look for the table in the current database */ + SELECT TOP 1 @OutputDatabaseName = QUOTENAME(TABLE_CATALOG) + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'BlitzFirst'; -UPDATE b -SET b.parameter_sniffing_symptoms = - SUBSTRING( - /*Duration*/ - CASE WHEN (b.min_duration * 100) < (b.avg_duration) THEN ', Fast sometimes' ELSE '' END + - CASE WHEN (b.max_duration) > (b.avg_duration * 100) THEN ', Slow sometimes' ELSE '' END + - CASE WHEN (b.last_duration * 100) < (b.avg_duration) THEN ', Fast last run' ELSE '' END + - CASE WHEN (b.last_duration) > (b.avg_duration * 100) THEN ', Slow last run' ELSE '' END + - /*CPU*/ - CASE WHEN (b.min_cpu_time / b.avg_dop) * 100 < (b.avg_cpu_time / b.avg_dop) THEN ', Low CPU sometimes' ELSE '' END + - CASE WHEN (b.max_cpu_time / b.max_dop) > (b.avg_cpu_time / b.avg_dop) * 100 THEN ', High CPU sometimes' ELSE '' END + - CASE WHEN (b.last_cpu_time / b.last_dop) * 100 < (b.avg_cpu_time / b.avg_dop) THEN ', Low CPU last run' ELSE '' END + - CASE WHEN (b.last_cpu_time / b.last_dop) > (b.avg_cpu_time / b.avg_dop) * 100 THEN ', High CPU last run' ELSE '' END + - /*Logical Reads*/ - CASE WHEN (b.min_logical_io_reads * 100) < (b.avg_logical_io_reads) THEN ', Low reads sometimes' ELSE '' END + - CASE WHEN (b.max_logical_io_reads) > (b.avg_logical_io_reads * 100) THEN ', High reads sometimes' ELSE '' END + - CASE WHEN (b.last_logical_io_reads * 100) < (b.avg_logical_io_reads) THEN ', Low reads last run' ELSE '' END + - CASE WHEN (b.last_logical_io_reads) > (b.avg_logical_io_reads * 100) THEN ', High reads last run' ELSE '' END + - /*Logical Writes*/ - CASE WHEN (b.min_logical_io_writes * 100) < (b.avg_logical_io_writes) THEN ', Low writes sometimes' ELSE '' END + - CASE WHEN (b.max_logical_io_writes) > (b.avg_logical_io_writes * 100) THEN ', High writes sometimes' ELSE '' END + - CASE WHEN (b.last_logical_io_writes * 100) < (b.avg_logical_io_writes) THEN ', Low writes last run' ELSE '' END + - CASE WHEN (b.last_logical_io_writes) > (b.avg_logical_io_writes * 100) THEN ', High writes last run' ELSE '' END + - /*Physical Reads*/ - CASE WHEN (b.min_physical_io_reads * 100) < (b.avg_physical_io_reads) THEN ', Low physical reads sometimes' ELSE '' END + - CASE WHEN (b.max_physical_io_reads) > (b.avg_physical_io_reads * 100) THEN ', High physical reads sometimes' ELSE '' END + - CASE WHEN (b.last_physical_io_reads * 100) < (b.avg_physical_io_reads) THEN ', Low physical reads last run' ELSE '' END + - CASE WHEN (b.last_physical_io_reads) > (b.avg_physical_io_reads * 100) THEN ', High physical reads last run' ELSE '' END + - /*Memory*/ - CASE WHEN (b.min_query_max_used_memory * 100) < (b.avg_query_max_used_memory) THEN ', Low memory sometimes' ELSE '' END + - CASE WHEN (b.max_query_max_used_memory) > (b.avg_query_max_used_memory * 100) THEN ', High memory sometimes' ELSE '' END + - CASE WHEN (b.last_query_max_used_memory * 100) < (b.avg_query_max_used_memory) THEN ', Low memory last run' ELSE '' END + - CASE WHEN (b.last_query_max_used_memory) > (b.avg_query_max_used_memory * 100) THEN ', High memory last run' ELSE '' END + - /*Duration*/ - CASE WHEN b.min_rowcount * 100 < b.avg_rowcount THEN ', Low row count sometimes' ELSE '' END + - CASE WHEN b.max_rowcount > b.avg_rowcount * 100 THEN ', High row count sometimes' ELSE '' END + - CASE WHEN b.last_rowcount * 100 < b.avg_rowcount THEN ', Low row count run' ELSE '' END + - CASE WHEN b.last_rowcount > b.avg_rowcount * 100 THEN ', High row count last run' ELSE '' END + - /*DOP*/ - CASE WHEN b.min_dop = 1 THEN ', Serial sometimes' ELSE '' END + - CASE WHEN b.max_dop > 1 THEN ', Parallel sometimes' ELSE '' END + - CASE WHEN b.last_dop = 1 THEN ', Serial last run' ELSE '' END + - CASE WHEN b.last_dop > 1 THEN ', Parallel last run' ELSE '' END + - /*tempdb*/ - CASE WHEN b.min_tempdb_space_used * 100 < b.avg_tempdb_space_used THEN ', Low tempdb sometimes' ELSE '' END + - CASE WHEN b.max_tempdb_space_used > b.avg_tempdb_space_used * 100 THEN ', High tempdb sometimes' ELSE '' END + - CASE WHEN b.last_tempdb_space_used * 100 < b.avg_tempdb_space_used THEN ', Low tempdb run' ELSE '' END + - CASE WHEN b.last_tempdb_space_used > b.avg_tempdb_space_used * 100 THEN ', High tempdb last run' ELSE '' END + - /*tlog*/ - CASE WHEN b.min_log_bytes_used * 100 < b.avg_log_bytes_used THEN ', Low log use sometimes' ELSE '' END + - CASE WHEN b.max_log_bytes_used > b.avg_log_bytes_used * 100 THEN ', High log use sometimes' ELSE '' END + - CASE WHEN b.last_log_bytes_used * 100 < b.avg_log_bytes_used THEN ', Low log use run' ELSE '' END + - CASE WHEN b.last_log_bytes_used > b.avg_log_bytes_used * 100 THEN ', High log use last run' ELSE '' END - , 2, 200000) -FROM #working_metrics AS b -OPTION (RECOMPILE); + IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') + SET @OutputDatabaseName = '[DBAtools]'; -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure analyzing parameter sniffing', 0,1) WITH NOWAIT; + END; - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL + OR NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; + RETURN; END; + IF @LogMessageCheckDate IS NULL + SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ' + + ' @SrvName, @LogMessageCheckDate, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)'; - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; + EXECUTE sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', + @LocalServerName, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; - RETURN; -END CATCH; + RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; -BEGIN TRY + RETURN; + END; -BEGIN +IF @SinceStartup = 1 + BEGIN + SET @Seconds = 0 + IF @ExpertMode = 0 + SET @ExpertMode = 1 + END; -IF (@Failed = 0 AND @ExportToExcel = 0 AND @SkipXML = 0) -BEGIN -RAISERROR(N'Returning regular results', 0, 1) WITH NOWAIT; +IF @OutputType = 'SCHEMA' +BEGIN + SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)'; +END; +ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL +BEGIN + /* They want to look into the past. */ + SET @AsOf1= DATEADD(mi, -15, @AsOf); + SET @AsOf2= DATEADD(mi, +15, @AsOf); -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.last_force_failure_reason_desc, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') SELECT CheckDate, [Priority], [FindingsGroup], [Finding], [URL], CAST([Details] AS [XML]) AS Details,' + + '[HowToStopIt], [CheckID], [StartTime], [LoginName], [NTUserName], [OriginalLoginName], [ProgramName], [HostName], [DatabaseID],' + + '[DatabaseName], [OpenTransactionCount], [QueryPlan], [QueryText] FROM ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' WHERE CheckDate >= @AsOf1' + + ' AND CheckDate <= @AsOf2' + + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; + EXEC sp_executesql @StringToExecute, + N'@AsOf1 DATETIMEOFFSET, @AsOf2 DATETIMEOFFSET', + @AsOf1, @AsOf2 -END; -IF (@Failed = 1 AND @ExportToExcel = 0 AND @SkipXML = 0) +END; /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ +ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ BEGIN + /* What's running right now? This is the first and last result set. */ + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_Start%' + BEGIN + IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL + BEGIN + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; + ELSE + BEGIN + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ -RAISERROR(N'Returning results for failed queries', 0, 1) WITH NOWAIT; + /* Set start/finish times AFTER sp_BlitzWho runs. For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2244 */ + IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + /* Use the most accurate (but undocumented) DMV if it's available: */ + IF EXISTS(SELECT * FROM sys.all_columns ac WHERE ac.object_id = OBJECT_ID('sys.dm_cloud_database_epoch') AND ac.name = 'last_role_transition_time') + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),last_role_transition_time) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.dm_cloud_database_epoch; + ELSE + WITH WaitTimes AS ( + SELECT wait_type, wait_time_ms, + NTILE(3) OVER(ORDER BY wait_time_ms) AS grouper + FROM sys.dm_os_wait_stats w + WHERE wait_type IN ('DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', + 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT') + ) + SELECT @StartSampleTime = DATEADD(mi, AVG(-wait_time_ms / 1000 / 60), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() + FROM WaitTimes + WHERE grouper = 2; + END + ELSE IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.databases + WHERE database_id = 2; + ELSE + SELECT @StartSampleTime = SYSDATETIMEOFFSET(), + @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), + @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()); -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, - wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); -END; + SELECT @logical_processors = COUNT(*) + FROM sys.dm_os_schedulers + WHERE status = 'VISIBLE ONLINE'; -IF (@ExportToExcel = 1 AND @SkipXML = 0) -BEGIN + IF EXISTS + ( -RAISERROR(N'Returning results for Excel export', 0, 1) WITH NOWAIT; + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.dm_os_schedulers') + AND ac.name = 'total_cpu_usage_ms' -UPDATE #working_plan_text -SET query_sql_text = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(query_sql_text)),' ','<>'),'><',''),'<>',' '), 1, 31000) -OPTION (RECOMPILE); + ) + BEGIN -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); + SELECT + @total_cpu_usage = 1, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_cpu_usage_ms) + ) + FROM sys.dm_os_schedulers AS s + WHERE s.status = ''VISIBLE ONLINE'' + AND s.is_online = 1 + OPTION(RECOMPILE); + '; -END; + END + ELSE + BEGIN + SELECT + @total_cpu_usage = 0, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_worker_time / 1000.) + ) + FROM sys.dm_exec_query_stats AS s + OPTION(RECOMPILE); + '; + END -IF (@ExportToExcel = 0 AND @SkipXML = 1) -BEGIN + RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; -RAISERROR(N'Returning results for skipped XML', 0, 1) WITH NOWAIT; + /* + We start by creating #BlitzFirstResults. It's a temp table that will store + the results from our checks. Throughout the rest of this stored procedure, + we're running a series of checks looking for dangerous things inside the SQL + Server. When we find a problem, we insert rows into the temp table. At the + end, we return these results to the end user. -WITH x AS ( -SELECT wpt.database_name, wm.plan_id, wm.query_id, wpt.query_sql_text, wpt.query_plan_xml, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.last_force_failure_reason_desc, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); + #BlitzFirstResults has a CheckID field, but there's no Check table. As we do + checks, we insert data into this table, and we manually put in the CheckID. + We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can + download that from http://FirstResponderKit.org if you want to build + a tool that relies on the output of sp_BlitzFirst. + */ -END; -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning results', 0,1) WITH NOWAIT; + IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL + DROP TABLE #BlitzFirstResults; + CREATE TABLE #BlitzFirstResults + ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NULL, + Details NVARCHAR(MAX) NULL, + HowToStopIt NVARCHAR(MAX) NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + QueryStatsNowID INT NULL, + QueryStatsFirstID INT NULL, + PlanHandle VARBINARY(64) NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) + ); - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; + IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL + DROP TABLE #WaitStats; + CREATE TABLE #WaitStats ( + Pass TINYINT NOT NULL, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + thread_time_ms FLOAT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT, + SampleTime datetimeoffset + ); - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; + IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL + DROP TABLE #FileStats; + CREATE TABLE #FileStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass TINYINT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + avg_stall_read_ms INT , + avg_stall_write_ms INT + ); - RETURN; -END CATCH; + IF OBJECT_ID('tempdb..#QueryStats') IS NOT NULL + DROP TABLE #QueryStats; + CREATE TABLE #QueryStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass INT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + [sql_handle] VARBINARY(64), + statement_start_offset INT, + statement_end_offset INT, + plan_generation_num BIGINT, + plan_handle VARBINARY(64), + execution_count BIGINT, + total_worker_time BIGINT, + total_physical_reads BIGINT, + total_logical_writes BIGINT, + total_logical_reads BIGINT, + total_clr_time BIGINT, + total_elapsed_time BIGINT, + creation_time DATETIMEOFFSET, + query_hash BINARY(8), + query_plan_hash BINARY(8), + Points TINYINT + ); -BEGIN TRY -BEGIN + IF OBJECT_ID('tempdb..#PerfmonStats') IS NOT NULL + DROP TABLE #PerfmonStats; + CREATE TABLE #PerfmonStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass TINYINT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL + ); -IF (@ExportToExcel = 0 AND @HideSummary = 0 AND @SkipXML = 0) -BEGIN - RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; + IF OBJECT_ID('tempdb..#PerfmonCounters') IS NOT NULL + DROP TABLE #PerfmonCounters; + CREATE TABLE #PerfmonCounters ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL + ); - /* Build summary data */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE frequent_execution = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1, - 100, - 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', - 'Queries are being executed more than ' - + CAST (@execution_threshold AS VARCHAR(5)) - + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; + IF OBJECT_ID('tempdb..#FilterPlansByDatabase') IS NOT NULL + DROP TABLE #FilterPlansByDatabase; + CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE parameter_sniffing = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 2, - 50, - 'Parameterization', - 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', - 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; + IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL + DROP TABLE #checkversion; + CREATE TABLE #checkversion ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) + ); - /* Forced execution plans */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_forced_plan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 3, - 5, - 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', - 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); + IF OBJECT_ID('tempdb..##WaitCategories') IS NULL + BEGIN + /* We reuse this one by default rather than recreate it every time. */ + CREATE TABLE ##WaitCategories + ( + WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, + WaitCategory NVARCHAR(128) NOT NULL, + Ignorable BIT DEFAULT 0 + ); + END; /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ + + IF 527 > (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) + BEGIN + TRUNCATE TABLE ##WaitCategories; + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_NETWORK_IO','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BACKUPIO','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_CONNECTION_RECEIVE_TASK','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_DISPATCHER','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_ENDPOINT_STATE_MUTEX','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_EVENTHANDLER','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_FORWARDER','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_INIT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_MASTERSTART','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_RECEIVE_WAITFOR','User Wait',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_REGISTERALLENDPOINTS','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SERVICE','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SHUTDOWN','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_START','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SHUTDOWN','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_STOP','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SUBMIT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TO_FLUSH','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_OBJECT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_TABLE','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_WORK','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXCONSUMER','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_SEND','Mirroring',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_TABLE_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DPT_ENTRY_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_STATE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_TMDOWN_REQUEST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_WAITFOR_OUTCOME','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_ENLIST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_PREPARE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_RECOVERY','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TM','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TRANSACTION_ENLISTMENT','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCPNTSYNC','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EE_PMOLOCK','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXCHANGE','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXTERNAL_SCRIPT_NETWORK_IOF','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_READ','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_WRITE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_METADATA_MUTEX','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_PROPERTYLIST_CACHE','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_RESTART_CRAWL','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FULLTEXT GATHERER','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AG_MUTEX','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_CRITICAL_SECTION_ENTRY','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_MANAGER_MUTEX','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_UNLOAD_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_BULK_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_QUEUE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CLUSAPI_CALL','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_COMPRESSED_CACHE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CONNECTIVITY_INFO','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_VERSIONING_STATE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RECOVERY','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RESTART','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_COMMAND','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_COMPLETION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_START_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER_FILTER_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_MANAGER','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_PREPROC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_GROUP_COMMIT','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_WAIT','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGPROGRESS_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_DEQUEUE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_STARTUP_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_TERMINATION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_PARTNER_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_READ_ALL_NETWORKS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_CONNECTION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_UNDO','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_REPLICAINFO_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_CANCELLATION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_FILE_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_LIMIT_BACKUPS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_SYNC_COMPLETION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_TIMEOUT_TASK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_WAIT_FOR_COMPLETION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNC_COMMIT','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNCHRONIZING_THROTTLE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC_PROCESSING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_THROTTLE_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TIMER_TASK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_DBRLIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_SESSION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_POOL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_QUEUE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_XRF_STACK_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('INSTANCE_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_QUEUE_LIMIT','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_RETRY','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_DT','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_EX','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_KP','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_NL','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_SH','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_UP','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LAZYWRITER_SLEEP','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOG_RATE_GOVERNOR','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_PMM_LOG','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_RESERVE_APPEND','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_ALLOCATION_EXT','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_GRANT_UPDATE','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MGR_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSSEARCH','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('NET_WAITFOR_PACKET','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ONDEMAND_TASK_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_DT','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_EX','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_KP','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_NL','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_SH','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_UP','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_DT','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_EX','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_KP','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_DRAIN_WORKER','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_LOG_CACHE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_LIST','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_TURN','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_SYNC','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_WAIT_WORK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POPULATE_LOCK_ORDINALS','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPVDIDEVICE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COCREATEINSTANCE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COGETCLASSOBJECT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_CREATEACCESSOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_DELETEROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETCOMMANDTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETNEXTROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETRESULT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETROWSBYBOOKMARK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBFLUSH','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBREADAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSETSIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSTAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBUNLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBWRITEAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_QUERYINTERFACE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEACCESSOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASESESSION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RESTARTPOSITION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREADANDWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETDATAFAILURE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSETSIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSTAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMUNLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CONSOLEWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CREATEPARAM','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DEBUG','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSADDLINK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKEXISTCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKHEALTHCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVELINK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVEROOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTFOLDERCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTSHARECHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORTREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_BEGINTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_COMMITREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ENLIST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_PREPAREREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FILESIZEGET','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_STARTTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_GETRMINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_EVENT_WAIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_REQUEST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_LOCKMONITOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_MSS_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ODBCOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLE_UNINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTTRAN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETDATASOURCE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETLITERALINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTYINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETSCHEMALOCK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_JOINTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_SETPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDBOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHENTICATIONOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHORIZATIONOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_BACKUPREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLOSEHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLUSTEROPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMPLETEAUTHTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COPYFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTACQUIRECONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTIMPORTKEY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DECRYPTMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETESECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEIOCONTROL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DIRSVC_NETWORKOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DISCONNECTNAMEDPIPE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DOMAINSERVICESOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DSGETDCNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DTCOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ENCRYPTMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FILEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FINDFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FORMATMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREECREDENTIALSHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREELIBRARY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GENERICOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETADDRINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETDISKFREESPACE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILEATTRIBUTES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILESIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETLONGPATHNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETPROCADDRESS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMEPATHNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LIBRARYOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOADLIBRARY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOGONUSER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOOKUPACCOUNTSID','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MESSAGEQUEUEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MOVEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETGROUPGETUSERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETGROUPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETLOCALGROUPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERMODALSGET','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_OPENDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PDH_WMI_INIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PIPEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PROCESSOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYREGISTRY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REMOVEDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REPORTEVENT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REVERTTOSELF','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_RSFXDEVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SECURITYOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SERVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETENDOFFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEPOINTER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEVALIDDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETNAMEDSECURITYINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQLCLROPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQMLAUNCH','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYSIGNATURE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYTRUST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VSSOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WAITFORSINGLEOBJECT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WINSOCKOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILEGATHER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WSASETLASTERROR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_REENLIST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_RESIZELOG','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDREDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDUNDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SB_STOPENDPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SERVER_STARTUP','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SETRMINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SHAREDMEM_GETDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SNIOPEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSHOST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSTESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STARTRM','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_CHECKPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_RECOVER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STRESSDRIVER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TRANSIMPORT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATESNAPSHOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETFINALIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TIMERRUN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XETESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ACTION_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CLUSTER_INTEGRATION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_FAILOVER_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_JOIN','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_OFFLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ONLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_POST_ONLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_SERVER_READY_CONNECTIONS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_WORKITEM_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADRSIM','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_ASYNC_QUEUE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_SHUTDOWN_QUEUE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QUERY_TRACEOUT','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REDO_THREAD_PENDING_WORK','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_CACHE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_HISTORYCACHE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_SCHEMA_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANFSINFO_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANHASHTABLE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANTEXTINFO_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPLICA_WRITES','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REQUEST_FOR_DEADLOCK_SEARCH','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESERVED_MEMORY_ALLOCATION_EXT','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE_QUERY_COMPILE','Compilation',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BPOOL_FLUSH','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BUFFERPOOL_HELPLW','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DCOMSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERDBREADY','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERMDREADY','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERUPGRADED','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MEMORYPOOL_ALLOCATEPAGES','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MSDBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_RETRY_VIRTUALALLOC','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_SYSTEMTASK','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TASK','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_WORK_DISPATCHER','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_DEADLOCK_DETECTION','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_QUANTUM_PUNISHMENT','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_BUFFER_FLUSH','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_BUFFER','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_READ_IO_COMPLETION','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_WRITE_IO_COMPLETION','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_PENDING_BUFFER_WRITERS','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_SHUTDOWN','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_WAIT_ENTRIES','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('THREADPOOL','Worker Thread',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACE_EVTNOTIF','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACEWRITE','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_DT','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_EX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_KP','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_NL','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_SH','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_UP','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRANSACTION_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('UCS_SESSION_REGISTRATION','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_FOR_RESULTS','User Wait',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAITFOR','User Wait',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITE_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITELOG','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_OWN_TRANSACTION','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_RECLAIM_SESSION','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTLOCKINFO','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTWORKSPACE_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); + END; /* IF SELECT SUM(1) FROM ##WaitCategories <> 527 */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); + IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL + DROP TABLE #MasterFiles; + CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); + /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ + IF (SERVERPROPERTY('EngineEdition') = 5 /*(SERVERPROPERTY('Edition')) = 'SQL Azure' */ + AND (OBJECT_ID('sys.master_files') IS NULL)) + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;'; + ELSE + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;'; + EXEC(@StringToExecute); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_forced_parameterized = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 5, - 50, - 'Parameterization', - 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', - 'Execution plans have been compiled with forced parameterization.') ; + IF @FilterPlansByDatabase IS NOT NULL + BEGIN + IF UPPER(LEFT(@FilterPlansByDatabase,4)) = 'USER' + BEGIN + INSERT INTO #FilterPlansByDatabase (DatabaseID) + SELECT database_id + FROM sys.databases + WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb'); + END; + ELSE + BEGIN + SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' + ;WITH a AS + ( + SELECT CAST(1 AS BIGINT) f, CHARINDEX(',', @FilterPlansByDatabase) t, 1 SEQ + UNION ALL + SELECT t + 1, CHARINDEX(',', @FilterPlansByDatabase, t + 1), SEQ + 1 + FROM a + WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 + ) + INSERT #FilterPlansByDatabase (DatabaseID) + SELECT DISTINCT db.database_id + FROM a + INNER JOIN sys.databases db ON LTRIM(RTRIM(SUBSTRING(@FilterPlansByDatabase, a.f, a.t - a.f))) = db.name + WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL + OPTION (MAXRECURSION 0); + END; + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_parallel = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 6, - 200, - 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', - 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; + IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + CREATE TABLE #ReadableDBs ( + database_id INT + ); - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.near_parallel = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 7, - 200, - 'Execution Plans', - 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', - 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; + IF 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; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.plan_warnings = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 8, - 50, - 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; + SET @StringToExecute = 'INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'; + EXEC(@StringToExecute); + + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.long_running = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 9, - 50, - 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', - 'Long running queries have been found. These are queries with an average duration longer than ' - + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) - + ' second(s). These queries should be investigated for additional tuning options.') ; + DECLARE @v DECIMAL(6,2), + @build INT, + @memGrantSortSupported BIT = 1; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.missing_index_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 10, - 50, - 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', - 'Queries found with missing indexes.'); + RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.downlevel_estimator = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 13, - 200, - 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', - 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); + INSERT INTO #checkversion (version) + SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + OPTION (RECOMPILE); - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.implicit_conversions = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 14, - 50, - 'Performance', - 'Implicit Conversions', - 'http://brentozar.com/go/implicit', - 'One or more queries are comparing two fields that are not of the same data type.') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE compile_timeout = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 18, - 50, - 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', - 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); + SELECT @v = common_version , + @build = build + FROM #checkversion + OPTION (RECOMPILE); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE compile_memory_limit_exceeded = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 19, - 50, - 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', - 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); + IF (@v < 11) + OR (@v = 11 AND @build < 6020) + OR (@v = 12 AND @build < 5000) + OR (@v = 13 AND @build < 1601) + SET @memGrantSortSupported = 0; - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE warning_no_join_predicate = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 20, - 10, - 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', - 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + AND ((@v = 13 AND @build >= 5337) /* This DMF causes assertion errors: https://support.microsoft.com/en-us/help/4490136/fix-assertion-error-occurs-when-you-use-sys-dm-exec-query-statistics-x */ + OR (@v = 14 AND @build >= 3162) + OR (@v >= 15) + OR (@v <= 12)) /* Azure */ + SET @dm_exec_query_statistics_xml = 1; - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE plan_multiple_plans = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 21, - 200, - 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', - 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE unmatched_index_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 22, - 100, - 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', - 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); + SET @StockWarningHeader = '', + @StockDetailsHeader = @StockDetailsHeader + ''; - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_trivial = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 24, - 100, - 'Execution Plans', - 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', - 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_forced_serial= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 25, - 10, - 'Execution Plans', - 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', - 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; + /* Get the instance name to use as a Perfmon counter prefix. */ + IF SERVERPROPERTY('EngineEdition') IN (5, 8) /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure'*/ + SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) + FROM sys.dm_os_performance_counters; + ELSE + BEGIN + SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;'; + EXEC(@StringToExecute); + SELECT @ServiceName = object_name FROM #PerfmonStats; + DELETE #PerfmonStats; + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_key_lookup_expensive= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 26, - 100, - 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', - 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; + /* Build a list of queries that were run in the last 10 seconds. + We're looking for the death-by-a-thousand-small-cuts scenario + where a query is constantly running, and it doesn't have that + big of an impact individually, but it has a ton of impact + overall. We're going to build this list, and then after we + finish our @Seconds sample, we'll compare our plan cache to + this list to see what ran the most. */ + + /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ + IF @CheckProcedureCache = 1 + BEGIN + RAISERROR('@CheckProcedureCache = 1, capturing first pass of plan cache',10,1) WITH NOWAIT; + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) + AND attr.attribute = ''dbid'';'; + END; + END; + ELSE + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) + AND attr.attribute = ''dbid'';'; + END; + END; + EXEC(@StringToExecute); + + /* Get the totals for the entire plan cache */ + INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) + SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) + FROM sys.dm_exec_query_stats qs; + END; /*IF @CheckProcedureCache = 1 */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_remote_query_expensive= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 28, - 100, - 'Execution Plans', - 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', - 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.trace_flags_session IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 29, - 100, - 'Trace Flags', - 'Session Level Trace Flags Enabled', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'Someone is enabling session level Trace Flags in a query.') ; + IF EXISTS (SELECT * + FROM tempdb.sys.all_objects obj + INNER JOIN tempdb.sys.all_columns col1 ON obj.object_id = col1.object_id AND col1.name = 'object_name' + INNER JOIN tempdb.sys.all_columns col2 ON obj.object_id = col2.object_id AND col2.name = 'counter_name' + INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' + WHERE obj.name LIKE '%CustomPerfmonCounters%') + BEGIN + SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters'; + EXEC(@StringToExecute); + END; + ELSE + BEGIN + /* Add our default Perfmon counters */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Group','Active Hadr Threads','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Received from Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Transport/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control Time (ms/sec)','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Resent Messages/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Sends to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Control Delay', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Controls/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commit Time', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commits/Sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Pending Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Ready Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Compression Cache misses/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log remaining for undo', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Send Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Recovery Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo blocked/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo Bytes Remaining', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redone Bytes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Bytes Flushed/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Pool LogWriter Pushes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL); + /* Below counters added by Jefferson Elias */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL); + /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. + And yes, they actually hard-coded the version numbers into the counters, and SQL 2019 still says 2017, oddly. + For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group + */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions created/sec',NULL); + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_unused_grant IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 30, - 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', - 'https://www.brentozar.com/blitzcache/unused-memory-grants/', - 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.function_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 31, - 100, - 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.clr_function_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 32, - 100, - 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; + /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. + After we finish doing our checks, we'll take another sample and compare them. */ + RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + CASE @Seconds + WHEN 0 + THEN 0 + ELSE @thread_time_ms + END AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + owt.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + 0 AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + os.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; + SET @StringToExecute = @StringToExecute + N' + ) x + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 + ) + GROUP BY x.Pass, x.SampleTime, x.wait_type + ORDER BY sum_wait_time_ms DESC;' + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 1 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 1 + OPTION(RECOMPILE); + + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , + num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + mf.[database_id], + mf.[file_id], + DB_NAME(vfs.database_id) AS [db_name], + mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , + CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_read_ms END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_reads END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_read] END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_write_ms END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_writes END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_written] END , + mf.physical_name, + mf.type_desc + FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs + INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id + AND vfs.database_id = mf.database_id + WHERE vfs.num_of_reads > 0 + OR vfs.num_of_writes > 0; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_table_variable = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 33, - 100, - 'Table Variables detected', - 'Beware nasty side effects', - 'https://www.brentozar.com/blitzcache/table-variables/', - 'All modifications are single threaded, and selects have really low row estimates.') ; + INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) + SELECT 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), CASE @Seconds WHEN 0 THEN 0 ELSE dmv.cntr_value END, dmv.cntr_type + FROM #PerfmonCounters counters + INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS + AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); + + /* For Github #2743: */ + CREATE TABLE #TempdbOperationalStats (object_id BIGINT PRIMARY KEY CLUSTERED, + forwarded_fetch_count BIGINT); + INSERT INTO #TempdbOperationalStats (object_id, forwarded_fetch_count) + SELECT object_id, forwarded_fetch_count + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID('tempdb'), NULL, NULL, NULL) os + WHERE os.database_id = DB_ID('tempdb') + AND os.forwarded_fetch_count > 100; + + + /* If they want to run sp_BlitzWho and export to table, go for it. */ + IF @OutputTableNameBlitzWho IS NOT NULL + AND @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('Logging sp_BlitzWho to table',10,1) WITH NOWAIT; + EXEC sp_BlitzWho @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzWho, @CheckDateOverride = @StartSampleTime, @OutputTableRetentionDays = @OutputTableRetentionDays; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.no_stats_warning = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 35, - 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', - 'https://www.brentozar.com/blitzcache/columns-no-statistics/', - 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; + RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.relop_warnings = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 36, - 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Check the plan for more details.') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_table_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 37, - 100, - 'Table Scans', - 'Your database has HEAPs', - 'https://www.brentozar.com/archive/2012/05/video-heaps/', - 'This may not be a problem. Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.backwards_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 38, - 100, - 'Backwards Scans', - 'Indexes are being read backwards', - 'https://www.brentozar.com/blitzcache/backwards-scans/', - 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; + /* Maintenance Tasks Running - Backup Running - CheckID 1 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 1',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.forced_index = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 39, - 100, - 'Index forcing', - 'Someone is using hints to force index usage', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans, and will prevent missing index requests.') ; + IF EXISTS(SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 300000 AND command LIKE 'BACKUP%') + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 1 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'Backup Running' AS Finding, + 'https://www.brentozar.com/askbrent/backups/' AS URL, + 'Backup 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) ' + @LineFeed + + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' + @LineFeed + + CASE WHEN COALESCE(s.nt_username, s.loginame) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_username, s.loginame) + ' ') ELSE '' END AS Details, + 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.[hostname] AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) 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.sysprocesses AS s ON r.session_id = s.spid AND s.ecid = 0 + INNER JOIN + ( + SELECT DISTINCT + t.request_session_id, + t.resource_database_id + FROM sys.dm_tran_locks AS t + WHERE t.resource_type = N'DATABASE' + AND t.request_mode = N'S' + AND t.request_status = N'GRANT' + AND t.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE' + ) AS db ON s.spid = db.request_session_id AND s.dbid = db.resource_database_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'BACKUP%' + AND r.start_time <= DATEADD(minute, -5, GETDATE()) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.forced_seek = 1 - OR p.forced_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 40, - 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; + /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ + IF @Seconds > 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure'*/ + BEGIN + SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; + EXEC(@StringToExecute); + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.columnstore_row_mode = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 41, - 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', - 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', - 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_computed_scalar = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 42, - 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', - 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', - 'This can cause a whole mess of bad serializartion problems.') ; + /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 2',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_sort_expensive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 43, - 100, - 'Execution Plans', - 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', - 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 2 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'DBCC CHECK* Running' AS Finding, + 'https://www.brentozar.com/askbrent/dbcc/' AS URL, + 'Corruption check 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) 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, + s.login_name AS LoginName, + 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, + 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id + FROM sys.dm_tran_locks l + INNER JOIN sys.databases d ON l.resource_database_id = d.database_id + WHERE l.resource_type = N'DATABASE' + AND l.request_mode = N'S' + AND l.request_status = N'GRANT' + AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) pl + OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t + WHERE r.command LIKE 'DBCC%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%ALTER INDEX%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%fileproperty%' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_computed_filter = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 44, - 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Someone put a Scalar UDF in the WHERE clause!') ; + /* Maintenance Tasks Running - Restore Running - CheckID 3 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 3',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.index_ops >= 5 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 45, - 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', - 'No URL yet', - 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'RESTORE%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 3 AS CheckID, + 1 AS Priority, + '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), + (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, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + 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 + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'RESTORE%' + AND s.program_name <> 'SQL Server Log Shipping' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_row_level = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', - 'No URL yet', - 'You may see a lot of confusing junk in your query plan.') ; + /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 4',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spatial = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 47, - 200, - 'Spatial Abuse', - 'You hit a Spatial Index', - 'No URL yet', - 'Purely informational.') ; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) + SELECT 4 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Database File Growing' AS Finding, + 'https://www.brentozar.com/go/instant' AS URL, + 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, + 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out https://www.brentozar.com/go/instant for details.' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + NULL AS DatabaseID, + NULL AS DatabaseName, + 0 AS OpenTransactionCount + FROM sys.dm_os_waiting_tasks t + INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id + INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END + + /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 5',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.index_dml = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 48, - 150, - 'Index DML', - 'Indexes were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 5 AS CheckID, + 1 AS Priority, + ''Query Problems'' AS FindingGroup, + ''Long-Running Query Blocking Others'' AS Finding, + ''https://www.brentozar.com/go/blocking'' AS URL, + ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows: ' + + @LineFeed + @LineFeed + + '''+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, + ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, + (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, + COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id)) AS QueryText, + r.start_time AS StartTime, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + r.[database_id] AS DatabaseID, + DB_NAME(r.database_id) AS DatabaseName, + 0 AS OpenTransactionCount, + r.query_hash + FROM sys.dm_os_waiting_tasks tBlocked + INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id + LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id + INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id + WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000 + /* And the blocking session ID is not blocked by anyone else: */ + AND NOT EXISTS(SELECT * FROM sys.dm_os_waiting_tasks tBlocking WHERE s.session_id = tBlocking.session_id AND tBlocking.session_id <> tBlocking.blocking_session_id AND tBlocking.blocking_session_id IS NOT NULL) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs);'; + EXECUTE sp_executesql @StringToExecute; + END; + + /* Query Problems - Plan Cache Erased Recently - CheckID 7 */ + IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 7',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.table_dml = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 49, - 150, - 'Table DML', - 'Tables were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT TOP 1 7 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Plan Cache Erased Recently' AS Finding, + 'https://www.brentozar.com/askbrent/plan-cache-erased-recently/' AS URL, + 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed + + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed + + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed + + 'SQL Server has to use a lot of CPU power in order to build execution' + @LineFeed + + 'plans and put them in cache again. This causes high CPU loads.' AS Details, + 'Find who did that, and stop them from doing it again.' AS HowToStopIt + FROM sys.dm_exec_query_stats + ORDER BY creation_time; + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.long_running_low_cpu = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 50, - 150, - 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', - 'No URL yet', - 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.low_cost_high_cpu = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 51, - 150, - 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', - 'No URL yet', - 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; + /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 8',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.stale_stats = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 52, - 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', - 'No URL yet', - 'Ever heard of updating statistics?') ; + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 8 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Sleeping Query with Open Transactions' AS Finding, + 'https://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, + 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.hostname + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_batch AS NVARCHAR(100)) + '. ' AS Details, + 'KILL ' + CAST(s.spid AS NVARCHAR(100)) + ';' AS HowToStopIt, + s.last_batch AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.hostname AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) AS DatabaseName, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + s.open_tran AS OpenTransactionCount + FROM sys.sysprocesses s + INNER JOIN sys.dm_exec_connections c ON s.spid = c.session_id + INNER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.spid = db.request_session_id + WHERE s.status = 'sleeping' + AND s.open_tran > 0 + AND s.last_batch < DATEADD(ss, -10, SYSDATETIME()) + AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.spid + AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')); + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_adaptive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', - 'No URL yet', - 'Joe Sack rules.') ; + /*Query Problems - Clients using implicit transactions - CheckID 37 */ + IF @Seconds > 0 + AND ( @@VERSION NOT LIKE 'Microsoft SQL Server 2005%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008 R2%' ) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 37',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spool_expensive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 54, - 150, - 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 37 AS CheckId, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Implicit Transactions'', + ''https://www.brentozar.com/go/ImplicitTransactions/'' AS URL, + ''Database: '' + DB_NAME(s.database_id) + '' '' + CHAR(13) + CHAR(10) + + ''Host: '' + s.[host_name] + '' '' + CHAR(13) + CHAR(10) + + ''Program: '' + s.[program_name] + '' '' + CHAR(13) + CHAR(10) + + CONVERT(NVARCHAR(10), s.open_transaction_count) + + '' open transactions since: '' + + CONVERT(NVARCHAR(30), tat.transaction_begin_time) + ''. '' + AS Details, + ''Run sp_BlitzWho and check the is_implicit_transaction column to spot the culprits. +If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, + tat.transaction_begin_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + s.database_id, + DB_NAME(s.database_id) AS DatabaseName, + NULL AS Querytext, + s.open_transaction_count AS OpenTransactionCount + FROM sys.dm_tran_active_transactions AS tat + LEFT JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + LEFT JOIN sys.dm_exec_sessions AS s + ON s.session_id = tst.session_id + WHERE tat.name = ''implicit_transaction''; + ' + EXECUTE sp_executesql @StringToExecute; + END; - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spool_more_rows = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 55, - 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; + /* Query Problems - Query Rolling Back - CheckID 9 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 9',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_bad_estimate = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 56, - 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', - 'No URL yet', - 'This may indicate a performance problem if mismatches occur regularly') ; + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, QueryHash) + SELECT 9 AS CheckID, + 1 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query Rolling Back' AS Finding, + 'https://www.brentozar.com/askbrent/rollback/' AS URL, + 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, + 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, + r.start_time AS StartTime, + s.login_name AS LoginName, + 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, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + r.query_hash + FROM sys.dm_exec_sessions s + INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + WHERE r.status = 'rollback'; + END - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_big_log = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 57, - 100, - 'High transaction log use', - 'This query on average uses more than half of the transaction log', - 'michaeljswart.com/2014/09/take-care-when-scripting-batches/', - 'This is probably a sign that you need to start batching queries') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_big_tempdb = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 58, - 100, - 'High tempdb use', - 'This query uses more than half of a data file on average', - 'No URL yet', - 'You should take a look at tempdb waits to see if you''re having problems') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_paul_white_electric = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 998, - 200, - 'Is Paul White Electric?', - 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT - 999, - 200, - 'Database Level Statistics', - 'The database ' + sa.[database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.last_update))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.modification_count)) + ' modifications on average.' AS Finding, - '' AS URL, - 'Consider updating statistics more frequently,' AS Details - FROM #stats_agg AS sa - GROUP BY sa.[database] - HAVING MAX(sa.last_update) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.modification_count) >= 100000; + IF @Seconds > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 1 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END - - IF EXISTS (SELECT 1/0 - FROM #trace_flags AS tf - WHERE tf.global_trace_flags IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1000, - 255, - 'Global Trace Flags Enabled', - 'You have Global Trace Flags enabled on your server', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; + /* Server Performance - Too Much Free Memory - CheckID 34 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 34',10,1) WITH NOWAIT; + END - IF EXISTS (SELECT 1/0 - FROM #working_plan_text AS p - WHERE p.min_grant_kb IS NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1001, - 255, - 'Plans not in cache', - 'We checked sys.dm_exec_query_stats for memory grant info', - '', - 'Plans in Query Store aren''t in other DMVs, which means we can''t get some information about them.') ; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 34 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Too Much Free Memory' AS Finding, + 'https://www.brentozar.com/go/freememory' AS URL, + CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, + 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt + FROM sys.dm_os_performance_counters cFree + INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' + AND cTotal.counter_name = N'Total Server Memory (KB) ' + WHERE cFree.object_name LIKE N'%Memory Manager%' + AND cFree.counter_name = N'Free Memory (KB) ' + AND CAST(cFree.cntr_value AS BIGINT) > 20480000000 + AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - /* - Return worsts - */ - WITH worsts AS ( - SELECT gi.flat_date, - gi.start_range, - gi.end_range, - gi.total_avg_duration_ms, - gi.total_avg_cpu_time_ms, - gi.total_avg_logical_io_reads_mb, - gi.total_avg_physical_io_reads_mb, - gi.total_avg_logical_io_writes_mb, - gi.total_avg_query_max_used_memory_mb, - gi.total_rowcount, - gi.total_avg_log_bytes_mb, - gi.total_avg_tempdb_space, - CONVERT(NVARCHAR(20), gi.flat_date) AS worst_date, - CASE WHEN DATEPART(HOUR, gi.start_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.start_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range)) + 'am ' - WHEN DATEPART(HOUR, gi.start_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range) -12) + 'pm ' - END AS worst_start_time, - CASE WHEN DATEPART(HOUR, gi.end_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.end_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range)) + 'am ' - WHEN DATEPART(HOUR, gi.end_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range) -12) + 'pm ' - END AS worst_end_time - FROM #grouped_interval AS gi - ), - duration_worst AS ( - SELECT TOP 1 'Your worst duration range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_duration_ms DESC - ), - cpu_worst AS ( - SELECT TOP 1 'Your worst cpu range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_cpu_time_ms DESC - ), - logical_reads_worst AS ( - SELECT TOP 1 'Your worst logical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_logical_io_reads_mb DESC - ), - physical_reads_worst AS ( - SELECT TOP 1 'Your worst physical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_physical_io_reads_mb DESC - ), - logical_writes_worst AS ( - SELECT TOP 1 'Your worst logical write range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_logical_io_writes_mb DESC - ), - memory_worst AS ( - SELECT TOP 1 'Your worst memory range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_query_max_used_memory_mb DESC - ), - rowcount_worst AS ( - SELECT TOP 1 'Your worst row count range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_rowcount DESC - ), - logbytes_worst AS ( - SELECT TOP 1 'Your worst log bytes range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_log_bytes_mb DESC - ), - tempdb_worst AS ( - SELECT TOP 1 'Your worst tempdb range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_tempdb_space DESC - ) - INSERT #warning_results ( CheckID, Priority, FindingsGroup, Finding, URL, Details ) - SELECT 1002, 255, 'Worsts', 'Worst Duration', 'N/A', duration_worst.msg - FROM duration_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst CPU', 'N/A', cpu_worst.msg - FROM cpu_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Reads', 'N/A', logical_reads_worst.msg - FROM logical_reads_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Physical Reads', 'N/A', physical_reads_worst.msg - FROM physical_reads_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Writes', 'N/A', logical_writes_worst.msg - FROM logical_writes_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Memory', 'N/A', memory_worst.msg - FROM memory_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Row Counts', 'N/A', rowcount_worst.msg - FROM rowcount_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Log Bytes', 'N/A', logbytes_worst.msg - FROM logbytes_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst tempdb', 'N/A', tempdb_worst.msg - FROM tempdb_worst - OPTION (RECOMPILE); + /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 35',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 35 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Target Memory Lower Than Max' AS Finding, + 'https://www.brentozar.com/go/target' AS URL, + N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed + + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, + 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt + FROM sys.configurations cMax + INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' + AND cTarget.counter_name = N'Target Server Memory (KB) ' + WHERE cMax.name = 'max server memory (MB)' + AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) + AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ + AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ + END - IF NOT EXISTS (SELECT 1/0 - FROM #warning_results AS bcr - WHERE bcr.Priority = 2147483646 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (2147483646, - 255, - 'Need more help?' , - 'Paste your plan on the internet!', - 'http://pastetheplan.com', - 'This makes it easy to share plans and post them to Q&A sites like https://dba.stackexchange.com/!') ; + /* Server Info - Database Size, Total GB - CheckID 21 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 21',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 21 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Database Size, Total GB' AS Finding, + CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, + SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM #MasterFiles + WHERE database_id > 4; - IF NOT EXISTS (SELECT 1/0 - FROM #warning_results AS bcr - WHERE bcr.Priority = 2147483647 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 2147483647, - 255, - 'Thanks for using sp_BlitzQueryStore!' , - 'From Your Community Volunteers', - 'http://FirstResponderKit.org', - 'We hope you found this tool useful. Current version: ' + @Version + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.') ; - + /* Server Info - Database Count - CheckID 22 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 22',10,1) WITH NOWAIT; + END - - SELECT Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - FROM #warning_results - GROUP BY Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - ORDER BY Priority ASC, CheckID ASC - OPTION (RECOMPILE); + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 22 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Database Count' AS Finding, + CAST(SUM(1) AS VARCHAR(100)) AS Details, + SUM (1) AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM sys.databases + WHERE database_id > 4; + /* Server Info - Memory Grants pending - CheckID 39 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 39',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 39 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Grants Pending' AS Finding, + CAST(PendingGrants.Details AS NVARCHAR(50)) AS Details, + PendingGrants.DetailsInt, + 'https://www.brentozar.com/blitz/memory-grants/' AS URL + FROM + ( + SELECT + COUNT(1) AS Details, + COUNT(1) AS DetailsInt + FROM sys.dm_exec_query_memory_grants AS Grants + WHERE queue_id IS NOT NULL + ) AS PendingGrants + WHERE PendingGrants.Details > 0; + + /* Server Info - Memory Grant/Workspace info - CheckID 40 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 40',10,1) WITH NOWAIT; + END -END; + DECLARE @MaxWorkspace BIGINT + SET @MaxWorkspace = (SELECT CAST(cntr_value AS BIGINT)/1024 FROM #PerfmonStats WHERE counter_name = N'Maximum Workspace Memory (KB)') + + IF (@MaxWorkspace IS NULL + OR @MaxWorkspace = 0) + BEGIN + SET @MaxWorkspace = 1 + END -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning warnings', 0,1) WITH NOWAIT; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 40 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Memory Grant/Workspace info' AS Finding, + + 'Grants Outstanding: ' + CAST((SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS NVARCHAR(50)) + @LineFeed + + 'Total Granted(MB): ' + CAST(ISNULL(SUM(Grants.granted_memory_kb) / 1024, 0) AS NVARCHAR(50)) + @LineFeed + + 'Total WorkSpace(MB): ' + CAST(ISNULL(@MaxWorkspace, 0) AS NVARCHAR(50)) + @LineFeed + + 'Granted workspace: ' + CAST(ISNULL((CAST(SUM(Grants.granted_memory_kb) / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + @LineFeed + + 'Oldest Grant in seconds: ' + CAST(ISNULL(DATEDIFF(SECOND, MIN(Grants.request_time), GETDATE()), 0) AS NVARCHAR(50)) AS Details, + (SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM sys.dm_exec_query_memory_grants AS Grants; + + /* Query Problems - Queries with high memory grants - CheckID 46 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 46',10,1) WITH NOWAIT; + END - IF @sql_select IS NOT NULL + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL, QueryText, QueryPlan) + SELECT 46 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query with a memory grant exceeding ' + +CAST(@MemoryGrantThresholdPct AS NVARCHAR(15)) + +'%' AS Finding, + 'Granted size: '+ CAST(CAST(Grants.granted_memory_kb / 1024 AS INT) AS NVARCHAR(50)) + +N'MB ' + + @LineFeed + +N'Granted pct of max workspace: ' + + CAST(ISNULL((CAST(Grants.granted_memory_kb / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + + @LineFeed + +N'SQLHandle: ' + +CONVERT(NVARCHAR(128),Grants.[sql_handle],1), + 'https://www.brentozar.com/memory-grants-sql-servers-public-toilet/' AS URL, + SQLText.[text], + QueryPlan.query_plan + FROM sys.dm_exec_query_memory_grants AS Grants + OUTER APPLY sys.dm_exec_sql_text(Grants.[sql_handle]) AS SQLText + OUTER APPLY sys.dm_exec_query_plan(Grants.[plan_handle]) AS QueryPlan + WHERE Grants.granted_memory_kb > ((@MemoryGrantThresholdPct/100.00)*(@MaxWorkspace*1024)); + + /* Query Problems - Memory Leak in USERSTORE_TOKENPERM Cache - CheckID 45 */ + IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_os_memory_clerks') AND name = 'pages_kb') BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45',10,1) WITH NOWAIT; + END - RETURN; -END CATCH; + /* SQL 2012+ version */ + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / SUM(pages_kb) >= 0.1 + AND SUM(pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + ELSE + BEGIN + /* Antiques Roadshow SQL 2008R2 - version */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45 (Legacy version)',10,1) WITH NOWAIT; + END -IF @Debug = 1 + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / SUM(single_pages_kb + multi_pages_kb) >= 0.1 + AND SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END -BEGIN TRY -BEGIN -RAISERROR(N'Returning debugging data from temp tables', 0, 1) WITH NOWAIT; + IF @Seconds > 0 + BEGIN ---Table content debugging + IF EXISTS ( SELECT 1/0 + FROM sys.all_objects AS ao + WHERE ao.name = 'dm_exec_query_profiles' ) + BEGIN -SELECT '#working_metrics' AS table_name, * -FROM #working_metrics AS wm -OPTION (RECOMPILE); + IF EXISTS( SELECT 1/0 + FROM sys.dm_exec_requests AS r + JOIN sys.dm_exec_sessions AS s + ON r.session_id = s.session_id + WHERE s.host_name IS NOT NULL + AND r.total_elapsed_time > 5000 + AND r.request_id > 0 ) + BEGIN -SELECT '#working_plan_text' AS table_name, * -FROM #working_plan_text AS wpt -OPTION (RECOMPILE); + SET @StringToExecute = N' + DECLARE @bad_estimate TABLE + ( + session_id INT, + request_id INT, + estimate_inaccuracy BIT + ); + + INSERT @bad_estimate ( session_id, request_id, estimate_inaccuracy ) + SELECT x.session_id, + x.request_id, + x.estimate_inaccuracy + FROM ( + SELECT deqp.session_id, + deqp.request_id, + CASE WHEN (deqp.row_count/10000) > deqp.estimate_row_count + THEN 1 + ELSE 0 + END AS estimate_inaccuracy + FROM sys.dm_exec_query_profiles AS deqp + INNER JOIN sys.dm_exec_requests r ON deqp.session_id = r.session_id AND deqp.request_id = r.request_id + WHERE deqp.session_id <> @@SPID + AND r.total_elapsed_time > 5000 + ) AS x + WHERE x.estimate_inaccuracy = 1 + GROUP BY x.session_id, + x.request_id, + x.estimate_inaccuracy; + + DECLARE @parallelism_skew TABLE + ( + session_id INT, + request_id INT, + parallelism_skew BIT + ); + + INSERT @parallelism_skew ( session_id, request_id, parallelism_skew ) + SELECT y.session_id, + y.request_id, + y.parallelism_skew + FROM ( + SELECT x.session_id, + x.request_id, + x.node_id, + x.thread_id, + x.row_count, + x.sum_node_rows, + x.node_dop, + x.sum_node_rows / x.node_dop AS even_distribution, + x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) AS skew_percent, + CASE + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) > 2. + THEN 1 + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) < 0.5 + THEN 1 + ELSE 0 + END AS parallelism_skew + FROM ( + SELECT deqp.session_id, + deqp.request_id, + deqp.node_id, + deqp.thread_id, + deqp.row_count, + SUM(deqp.row_count) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS sum_node_rows, + COUNT(*) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS node_dop + FROM sys.dm_exec_query_profiles AS deqp + WHERE deqp.thread_id > 0 + AND deqp.session_id <> @@SPID + AND EXISTS + ( + SELECT 1/0 + FROM sys.dm_exec_query_profiles AS deqp2 + WHERE deqp.session_id = deqp2.session_id + AND deqp.node_id = deqp2.node_id + AND deqp2.thread_id > 0 + GROUP BY deqp2.session_id, deqp2.node_id + HAVING COUNT(deqp2.node_id) > 1 + ) + ) AS x + ) AS y + WHERE y.parallelism_skew = 1 + GROUP BY y.session_id, + y.request_id, + y.parallelism_skew; + + /* Queries in dm_exec_query_profiles showing signs of poor cardinality estimates - CheckID 42 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 42'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 42 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x cardinality misestimations'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(b.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a large cardinality misestimate'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @bad_estimate AS b + JOIN sys.dm_exec_requests AS r + ON r.session_id = b.session_id + AND r.request_id = b.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = b.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + /* GitHub #3210 */ + SET @StringToExecute = N' + SET LOCK_TIMEOUT 1000 ' + @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + SET @StringToExecute = @StringToExecute + N'; + + /* Queries in dm_exec_query_profiles showing signs of unbalanced parallelism - CheckID 43 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 43'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 43 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x skewed parallelism'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(p.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a parallel threads doing uneven work.'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @parallelism_skew AS p + JOIN sys.dm_exec_requests AS r + ON r.session_id = p.session_id + AND r.request_id = p.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = p.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + SET @StringToExecute = @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + + SET @StringToExecute = @StringToExecute + N';'; + + EXECUTE sp_executesql @StringToExecute, N'@Debug BIT',@Debug = @Debug; + END + + END + END -SELECT '#working_warnings' AS table_name, * -FROM #working_warnings AS ww -OPTION (RECOMPILE); + /* Server Performance - High CPU Utilization - CheckID 24 */ + IF @Seconds < 30 + BEGIN + /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. + We get this data from the ring buffers, and it's only updated once per minute, so might + as well get it now - whereas if we're checking 30+ seconds, it might get updated by the + end of our sp_BlitzFirst session. */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; + END -SELECT '#working_wait_stats' AS table_name, * -FROM #working_wait_stats wws -OPTION (RECOMPILE); + 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' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; -SELECT '#grouped_interval' AS table_name, * -FROM #grouped_interval -OPTION (RECOMPILE); + /* CPU Utilization - CheckID 23 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + END -SELECT '#working_plans' AS table_name, * -FROM #working_plans -OPTION (RECOMPILE); + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + WITH y + AS + ( + SELECT CONVERT(VARCHAR(5), 100 - ca.c.value('.', 'INT')) AS system_idle, + CONVERT(VARCHAR(30), rb.event_date) AS event_date, + CONVERT(VARCHAR(8000), rb.record) AS record, + event_date as event_date_raw + FROM + ( SELECT CONVERT(XML, dorb.record) AS record, + DATEADD(ms, -( ts.ms_ticks - dorb.timestamp ), GETDATE()) AS event_date + FROM sys.dm_os_ring_buffers AS dorb + CROSS JOIN + ( SELECT dosi.ms_ticks FROM sys.dm_os_sys_info AS dosi ) AS ts + 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) + ) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL, HowToStopIt) + SELECT TOP 1 + 23, + 250, + 'Server Info', + 'CPU Utilization', + y.system_idle + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), + y.system_idle , + 'https://www.brentozar.com/go/cpu', + STUFF(( SELECT TOP 2147483647 + CHAR(10) + CHAR(13) + + y2.system_idle + + '% ON ' + + y2.event_date + + ' Ring buffer details: ' + + y2.record + FROM y AS y2 + ORDER BY y2.event_date_raw DESC + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'VARCHAR(MAX)'), 1, 1, N'') AS query + FROM y + ORDER BY y.event_date_raw DESC; -SELECT '#stats_agg' AS table_name, * -FROM #stats_agg -OPTION (RECOMPILE); + + /* Highlight if non SQL processes are using >25% CPU - CheckID 28 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 28',10,1) WITH NOWAIT; + END -SELECT '#trace_flags' AS table_name, * -FROM #trace_flags -OPTION (RECOMPILE); + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + ,record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLUsage + 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 - (y.SQLUsage + y.SystemIdle) >= 25; + + END; /* IF @Seconds < 30 */ -SELECT '#statements' AS table_name, * -FROM #statements AS s -OPTION (RECOMPILE); + /* Query Problems - Statistics Updated Recently - CheckID 44 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 44',10,1) WITH NOWAIT; + END -SELECT '#query_plan' AS table_name, * -FROM #query_plan AS qp -OPTION (RECOMPILE); + IF 20 >= (SELECT COUNT(*) FROM sys.databases WHERE name NOT IN ('master', 'model', 'msdb', 'tempdb')) + AND @Seconds > 0 + BEGIN + CREATE TABLE #UpdatedStats (HowToStopIt NVARCHAR(4000), RowsForSorting BIGINT); + IF EXISTS(SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_properties') + BEGIN + /* We don't want to hang around to obtain locks */ + SET LOCK_TIMEOUT 0; -SELECT '#relop' AS table_name, * -FROM #relop AS r -OPTION (RECOMPILE); + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SET @StringToExecute = N'USE [?];' + @LineFeed; + ELSE + SET @StringToExecute = N''; + + SET @StringToExecute = @StringToExecute + 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET LOCK_TIMEOUT 1000;' + @LineFeed + + 'BEGIN TRY' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) + N''.'' +' + @LineFeed + + ' QUOTENAME(SCHEMA_NAME(obj.schema_id)) + N''.'' +' + @LineFeed + + ' QUOTENAME(obj.name) +' + @LineFeed + + ' N'' statistic '' + QUOTENAME(stat.name) + ' + @LineFeed + + ' N'' was updated on '' + CONVERT(NVARCHAR(50), sp.last_updated, 121) + N'','' + ' + @LineFeed + + ' N'' had '' + CAST(sp.rows AS NVARCHAR(50)) + N'' rows, with '' +' + @LineFeed + + ' CAST(sp.rows_sampled AS NVARCHAR(50)) + N'' rows sampled,'' + ' + @LineFeed + + ' N'' producing '' + CAST(sp.steps AS NVARCHAR(50)) + N'' steps in the histogram.'',' + @LineFeed + + ' sp.rows' + @LineFeed + + ' FROM sys.objects AS obj WITH (NOLOCK)' + @LineFeed + + ' INNER JOIN sys.stats AS stat WITH (NOLOCK) ON stat.object_id = obj.object_id ' + @LineFeed + + ' CROSS APPLY sys.dm_db_stats_properties(stat.object_id, stat.stats_id) AS sp ' + @LineFeed + + ' WHERE sp.last_updated > DATEADD(MI, -15, GETDATE())' + @LineFeed + + ' AND obj.is_ms_shipped = 0' + @LineFeed + + ' AND ''[?]'' <> ''[tempdb]'';' + @LineFeed + + 'END TRY' + @LineFeed + + 'BEGIN CATCH' + @LineFeed + + ' IF (ERROR_NUMBER() = 1222)' + @LineFeed + + ' BEGIN ' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as the lock timeout was exceeded,''+' + @LineFeed + + ' N'' this is likely due to an Index operation in Progress'',' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + ' ELSE' + @LineFeed + + ' BEGIN' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as a result of error: ''+' + @LineFeed + + ' CAST(ERROR_NUMBER() AS NVARCHAR(10)) +' + @LineFeed + + ' N'' with message: ''+' + @LineFeed + + ' CAST(ERROR_MESSAGE() AS NVARCHAR(128)),' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + 'END CATCH' + ; + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + BEGIN TRY + EXEC sp_MSforeachdb @StringToExecute; + END TRY + BEGIN CATCH + IF (ERROR_NUMBER() = 1222) + BEGIN + INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting) + SELECT HowToStopIt = N'No information could be retrieved as the lock timeout was exceeded while iterating databases,' + + N' this is likely due to an Index operation in Progress', -1; + END + ELSE + BEGIN + THROW; + END + END CATCH + END + ELSE + EXEC(@StringToExecute); -SELECT '#plan_cost' AS table_name, * -FROM #plan_cost AS pc -OPTION (RECOMPILE); + /* Set timeout back to a default value of -1 */ + SET LOCK_TIMEOUT -1; + END; + + /* We mark timeout exceeded with a -1 so only show these IF there is statistics info that succeeded */ + IF EXISTS (SELECT * FROM #UpdatedStats WHERE RowsForSorting > -1) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 44 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Statistics Updated Recently' AS Finding, + 'https://www.brentozar.com/go/stats' AS URL, + 'In the last 15 minutes, statistics were updated. To see which ones, click the HowToStopIt column.' + @LineFeed + @LineFeed + + 'This effectively clears the plan cache for queries that involve these tables,' + @LineFeed + + 'which thereby causes parameter sniffing: those queries are now getting brand new' + @LineFeed + + 'query plans based on whatever parameters happen to call them next.' + @LineFeed + @LineFeed + + 'Be on the lookout for sudden parameter sniffing issues after this time range.', + HowToStopIt = (SELECT (SELECT HowToStopIt + NCHAR(10)) + FROM #UpdatedStats + ORDER BY RowsForSorting DESC + FOR XML PATH('')); -SELECT '#est_rows' AS table_name, * -FROM #est_rows AS er -OPTION (RECOMPILE); + END -SELECT '#stored_proc_info' AS table_name, * -FROM #stored_proc_info AS spi -OPTION(RECOMPILE); + /* 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 -SELECT '#conversion_info' AS table_name, * -FROM #conversion_info AS ci -OPTION ( RECOMPILE ); -SELECT '#variable_info' AS table_name, * -FROM #variable_info AS vi -OPTION ( RECOMPILE ); + /* Potential Upcoming Problems - High Number of Connections - CheckID 49 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 49',10,1) WITH NOWAIT; + END + IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%64%' AND SERVERPROPERTY('EngineEdition') <> 5 + BEGIN + IF @logical_processors <= 4 + SET @max_worker_threads = 512; + ELSE IF @logical_processors > 64 AND + ((@v = 13 AND @build >= 5026) OR @v >= 14) + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 32) + ELSE + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 16) -SELECT '#missing_index_xml' AS table_name, * -FROM #missing_index_xml -OPTION ( RECOMPILE ); + IF @max_worker_threads > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 49 AS CheckID, + 210 AS Priority, + 'Potential Upcoming Problems' AS FindingGroup, + 'High Number of Connections' AS Finding, + 'https://www.brentozar.com/archive/2014/05/connections-slow-sql-server-threadpool/' AS URL, + 'There are ' + CAST(SUM(1) AS VARCHAR(20)) + ' open connections, which would lead to ' + @LineFeed + 'worker thread exhaustion and THREADPOOL waits' + @LineFeed + 'if they all ran queries at the same time.' AS Details + FROM sys.dm_exec_connections c + HAVING SUM(1) > @max_worker_threads; + END + END -SELECT '#missing_index_schema' AS table_name, * -FROM #missing_index_schema -OPTION ( RECOMPILE ); -SELECT '#missing_index_usage' AS table_name, * -FROM #missing_index_usage -OPTION ( RECOMPILE ); -SELECT '#missing_index_detail' AS table_name, * -FROM #missing_index_detail -OPTION ( RECOMPILE ); + /* 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 -SELECT '#missing_index_pretty' AS table_name, * -FROM #missing_index_pretty -OPTION ( RECOMPILE ); -END; + RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning debug temp tables', 0,1) WITH NOWAIT; - IF @sql_select IS NOT NULL + /* End of checks. If we haven't waited @Seconds seconds, wait. */ + IF DATEADD(SECOND,1,SYSDATETIMEOFFSET()) < @FinishSampleTime BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; + WAITFOR TIME @FinishSampleTimeWaitFor; END; - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -/* -Ways to run this thing + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END ---Debug -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Debug = 1 + RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; + /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + @thread_time_ms AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + owt.wait_type, + SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + os.wait_type, + SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, + SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, + SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; ---Get the top 1 -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @Debug = 1 + SET @StringToExecute = @StringToExecute + N' + ) x + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 + ) + GROUP BY x.Pass, x.SampleTime, x.wait_type + ORDER BY sum_wait_time_ms DESC;'; + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 2 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 2 + OPTION(RECOMPILE); + ---Use a StartDate -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StartDate = '20170527' - ---Use an EndDate -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @EndDate = '20170527' - ---Use Both -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StartDate = '20170526', @EndDate = '20170527' + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , + num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) + SELECT 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + mf.[database_id], + mf.[file_id], + DB_NAME(vfs.database_id) AS [db_name], + mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , + CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , + vfs.io_stall_read_ms , + vfs.num_of_reads , + vfs.[num_of_bytes_read], + vfs.io_stall_write_ms , + vfs.num_of_writes , + vfs.[num_of_bytes_written], + mf.physical_name, + mf.type_desc, + 0, + 0 + FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs + INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id + AND vfs.database_id = mf.database_id + WHERE vfs.num_of_reads > 0 + OR vfs.num_of_writes > 0; ---Set a minimum execution count -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @MinimumExecutionCount = 10 + INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) + SELECT 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), dmv.cntr_value, dmv.cntr_type + FROM #PerfmonCounters counters + INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS + AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); ---Set a duration minimum -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @DurationFilter = 5 + /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ + UPDATE fNow + SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms + WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0; ---Look for a stored procedure name (that doesn't exist!) -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'blah' + UPDATE fNow + SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms + WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0; ---Look for a stored procedure name that does (at least On My Computer®) -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'UserReportExtended' + UPDATE pNow + SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, + [value_per_second] = ((1.0 * pNow.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime)) + FROM #PerfmonStats pNow + INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pNow.[object_name] AND pFirst.counter_name = pNow.counter_name AND (pFirst.instance_name = pNow.instance_name OR (pFirst.instance_name IS NULL AND pNow.instance_name IS NULL)) + AND pNow.ID > pFirst.ID + WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; ---Look for failed queries -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @Failed = 1 ---Filter by plan_id -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @PlanIdFilter = 3356 + /* Query Stats - If we're within 10 seconds of our projected finish time, do the plan cache analysis. - CheckID 18 */ + IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 18',10,1) WITH NOWAIT; + END ---Filter by query_id -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @QueryIdFilter = 2958 + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'https://www.brentozar.com/go/topqueries', + 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.'); -*/ + END; + ELSE IF @CheckProcedureCache = 1 + BEGIN -END; -GO -IF OBJECT_ID('dbo.sp_BlitzWho') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzWho AS RETURN 0;') -GO + RAISERROR('@CheckProcedureCache = 1, capturing second pass of plan cache',10,1) WITH NOWAIT; -ALTER PROCEDURE dbo.sp_BlitzWho - @Help TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0, - @ExpertMode BIT = 0, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -AS -BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; + /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= @StartSampleTimeText + AND attr.attribute = ''dbid'';'; + END; + END; + ELSE + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= @StartSampleTimeText + AND attr.attribute = ''dbid'';'; + END; + END; + /* Old version pre-2016/06/13: + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + ELSE + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + */ + SET @ParmDefinitions = N'@StartSampleTimeText NVARCHAR(100)'; + SET @Parm1 = CONVERT(NVARCHAR(100), CAST(@StartSampleTime AS DATETIME), 127); + EXECUTE sp_executesql @StringToExecute, @ParmDefinitions, @StartSampleTimeText = @Parm1; - IF @Help = 1 - PRINT ' -sp_BlitzWho from http://FirstResponderKit.org + RAISERROR('@CheckProcedureCache = 1, totaling up plan cache metrics',10,1) WITH NOWAIT; -This script gives you a snapshot of everything currently executing on your SQL Server. + /* Get the totals for the entire plan cache */ + INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) + SELECT 0 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) + FROM sys.dm_exec_query_stats qs; -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. -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - -MIT License + RAISERROR('@CheckProcedureCache = 1, so analyzing execution plans',10,1) WITH NOWAIT; + /* + Pick the most resource-intensive queries to review. Update the Points field + in #QueryStats - if a query is in the top 10 for logical reads, CPU time, + duration, or execution, add 1 to its points. + */ + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_elapsed_time > qsFirst.total_elapsed_time + AND qsNow.Pass = 2 + AND qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ + ORDER BY (qsNow.total_elapsed_time - COALESCE(qsFirst.total_elapsed_time, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; -Copyright (c) 2017 Brent Ozar Unlimited + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_logical_reads > qsFirst.total_logical_reads + AND qsNow.Pass = 2 + AND qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ + ORDER BY (qsNow.total_logical_reads - COALESCE(qsFirst.total_logical_reads, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; -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: + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_worker_time > qsFirst.total_worker_time + AND qsNow.Pass = 2 + AND qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */ + ORDER BY (qsNow.total_worker_time - COALESCE(qsFirst.total_worker_time, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.execution_count > qsFirst.execution_count + AND qsNow.Pass = 2 + AND (qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ + OR qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ + OR qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */) + ORDER BY (qsNow.execution_count - COALESCE(qsFirst.execution_count, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; -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. -'; + /* Query Stats - Most Resource-Intensive Queries - CheckID 17 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 17',10,1) WITH NOWAIT; + END -/* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@EnhanceFlag BIT = 0 - ,@StringToExecute NVARCHAR(MAX) - ,@EnhanceSQL NVARCHAR(MAX) = - N'query_stats.last_dop, - query_stats.min_dop, - query_stats.max_dop, - query_stats.last_grant_kb, - query_stats.min_grant_kb, - query_stats.max_grant_kb, - query_stats.last_used_grant_kb, - query_stats.min_used_grant_kb, - query_stats.max_used_grant_kb, - query_stats.last_ideal_grant_kb, - query_stats.min_ideal_grant_kb, - query_stats.max_ideal_grant_kb, - query_stats.last_reserved_threads, - query_stats.min_reserved_threads, - query_stats.max_reserved_threads, - query_stats.last_used_threads, - query_stats.min_used_threads, - query_stats.max_used_threads,' - ,@SessionWaits BIT = 0 - ,@SessionWaitsSQL NVARCHAR(MAX) = - N'LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT TOP 5 waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_time_ms) AS NVARCHAR(128)) - + N'' ms), '' - FROM sys.dm_exec_session_wait_stats AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - HAVING SUM(waitwait.wait_time_ms) > 5 - ORDER BY SUM(waitwait.wait_time_ms) DESC - FOR - XML PATH('''') ) AS session_wait_info - FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 - ON s.session_id = wt2.session_id - LEFT JOIN sys.dm_exec_query_stats AS session_stats - ON r.sql_handle = session_stats.sql_handle - AND r.plan_handle = session_stats.plan_handle - AND r.statement_start_offset = session_stats.statement_start_offset - AND r.statement_end_offset = session_stats.statement_end_offset' - ,@QueryStatsXML BIT = 0 - ,@QueryStatsXMLselect NVARCHAR(MAX) = N' CAST(COALESCE(qs_live.query_plan, '''') AS XML) AS live_query_plan , ' - ,@QueryStatsXMLSQL NVARCHAR(MAX) = N'OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle, QueryHash) + SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'https://www.brentozar.com/go/topqueries', + 'Query stats during the sample:' + @LineFeed + + 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + + 'CPU Time: ' + CAST(qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Logical Reads: ' + CAST(qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Logical Writes: ' + CAST(qsNow.total_logical_writes - (COALESCE(qsFirst.total_logical_writes, 0)) AS NVARCHAR(100)) + @LineFeed + + 'CLR Time: ' + CAST(qsNow.total_clr_time - (COALESCE(qsFirst.total_clr_time, 0)) AS NVARCHAR(100)) + @LineFeed + + @LineFeed + @LineFeed + 'Query stats since ' + CONVERT(NVARCHAR(100), qsNow.creation_time ,121) + @LineFeed + + 'Executions: ' + CAST(qsNow.execution_count AS NVARCHAR(100)) + + CASE qsTotal.execution_count WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time AS NVARCHAR(100)) + + CASE qsTotal.total_elapsed_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'CPU Time: ' + CAST(qsNow.total_worker_time AS NVARCHAR(100)) + + CASE qsTotal.total_worker_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Logical Reads: ' + CAST(qsNow.total_logical_reads AS NVARCHAR(100)) + + CASE qsTotal.total_logical_reads WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Logical Writes: ' + CAST(qsNow.total_logical_writes AS NVARCHAR(100)) + + CASE qsTotal.total_logical_writes WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_writes / qsTotal.total_logical_writes AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'CLR Time: ' + CAST(qsNow.total_clr_time AS NVARCHAR(100)) + + CASE qsTotal.total_clr_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_clr_time / qsTotal.total_clr_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + --@LineFeed + @LineFeed + 'Query hash: ' + CAST(qsNow.query_hash AS NVARCHAR(100)) + @LineFeed + + --@LineFeed + @LineFeed + 'Query plan hash: ' + CAST(qsNow.query_plan_hash AS NVARCHAR(100)) + + @LineFeed AS Details, + 'See the URL for tuning tips on why this query may be consuming resources.' AS HowToStopIt, + qp.query_plan, + QueryText = SUBSTRING(st.text, + (qsNow.statement_start_offset / 2) + 1, + ((CASE qsNow.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qsNow.statement_end_offset + END - qsNow.statement_start_offset) / 2) + 1), + qsNow.ID AS QueryStatsNowID, + qsFirst.ID AS QueryStatsFirstID, + qsNow.plan_handle AS PlanHandle, + qsNow.query_hash + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp + WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL; + UPDATE #BlitzFirstResults + SET DatabaseID = CAST(attr.value AS INT), + DatabaseName = DB_NAME(CAST(attr.value AS INT)) + FROM #BlitzFirstResults + CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr + WHERE attr.attribute = 'dbid'; -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) -IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan') - SET @QueryStatsXML = 1; + END; /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ -IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 -BEGIN -SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan , - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), - blocked.waittime) + '')'' ) AS wait_info , - CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name , - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' -END -IF @ProductVersionMajor >= 11 -BEGIN -SELECT @EnhanceFlag = - CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 - WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 - WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 - ELSE 0 - END + RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; + /* Wait Stats - CheckID 6 */ + /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 6',10,1) WITH NOWAIT; + END -IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL -BEGIN - SET @SessionWaits = 1 -END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) + SELECT TOP 10 6 AS CheckID, + 200 AS Priority, + 'Wait Stats' AS FindingGroup, + wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ + N'https://www.sqlskills.com/help/waits/' + LOWER(wNow.wait_type) + '/' AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt + FROM #WaitStats wNow + LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime + WHERE wNow.wait_time_ms > (wBase.wait_time_ms + (.5 * (DATEDIFF(ss,@StartSampleTime,@FinishSampleTime)) * 1000)) /* Only look for things we've actually waited on for half of the time or more */ + ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; + /* Server Performance - Poison Wait Detected - CheckID 30 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 30',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) + SELECT 30 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Poison Wait Detected: ' + wNow.wait_type AS Finding, + N'https://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt + FROM #WaitStats wNow + LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime + WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'POOL_LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') + AND wNow.wait_time_ms > (wBase.wait_time_ms + 1000); -SELECT @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan ,' - + - CASE @QueryStatsXML - WHEN 1 THEN + @QueryStatsXMLselect - ELSE N'' - END - +' - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) AS wait_info ,' - + - CASE @SessionWaits - WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' - ELSE N'' - END - + - N'CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , ' - + - CASE @EnhanceFlag - WHEN 1 THEN @EnhanceSQL - ELSE N'' - END - + - N' - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name, - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - ' - + - CASE @SessionWaits - WHEN 1 THEN @SessionWaitsSQL - ELSE N'' - END - + - ' - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - ' - + - CASE @QueryStatsXML - WHEN 1 THEN @QueryStatsXMLSQL - ELSE N'' - END - + - ' - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' + /* Server Performance - Slow Data File Reads - CheckID 11 */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 11',10,1) WITH NOWAIT; + END -END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) + SELECT TOP 10 11 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Slow Data File Reads' AS Finding, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, + 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed + + 'File: ' + fNow.PhysicalName + @LineFeed + + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed + + 'Seconds spent waiting on storage for these reads: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed + + 'Average read latency during the sample: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed + + 'Microsoft guidance for data file read speed: 20ms or less.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + fNow.DatabaseID, + fNow.DatabaseName + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > (fBase.io_stall_read_ms + 1000) + WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS + AND fNow.TypeDesc = 'ROWS' + ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; + END; -IF @Debug = 1 + /* Server Performance - Slow Log File Writes - CheckID 12 */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') BEGIN - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 160000)) + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 12',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) + SELECT TOP 10 12 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Slow Log File Writes' AS Finding, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, + 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed + + 'File: ' + fNow.PhysicalName + @LineFeed + + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed + + 'Seconds spent waiting on storage for these writes: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed + + 'Average write latency during the sample: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed + + 'Microsoft guidance for log file write speed: 3ms or less.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + fNow.DatabaseID, + fNow.DatabaseName + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > (fBase.io_stall_write_ms + 1000) + WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS + AND fNow.TypeDesc = 'LOG' + ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; + 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 + RAISERROR('Running CheckID 13',10,1) WITH NOWAIT; END -EXEC(@StringToExecute); + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 13 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Log File Growing' AS Finding, + 'https://www.brentozar.com/askbrent/file-growing/' AS URL, + 'Number of growths 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, + 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND object_name = @ServiceName + ':Databases' + AND counter_name = 'Log Growths' + AND value_delta > 0; -END -GO -IF OBJECT_ID('dbo.sp_DatabaseRestore') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_DatabaseRestore AS RETURN 0;'); -GO -ALTER PROCEDURE [dbo].[sp_DatabaseRestore] - @Database NVARCHAR(128) = NULL, - @RestoreDatabaseName NVARCHAR(128) = NULL, - @BackupPathFull NVARCHAR(MAX) = NULL, - @BackupPathDiff NVARCHAR(MAX) = NULL, - @BackupPathLog NVARCHAR(MAX) = NULL, - @MoveFiles BIT = 0, - @MoveDataDrive NVARCHAR(260) = NULL, - @MoveLogDrive NVARCHAR(260) = NULL, - @TestRestore BIT = 0, - @RunCheckDB BIT = 0, - @RestoreDiff BIT = 0, - @ContinueLogs BIT = 0, - @StandbyMode BIT = 0, - @StandbyUndoPath NVARCHAR(MAX) = NULL, - @RunRecovery BIT = 0, - @ForceSimpleRecovery BIT = 0, - @StopAt NVARCHAR(14) = NULL, - @OnlyLogsAfter NVARCHAR(14) = NULL, - @Debug INT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -AS -SET NOCOUNT ON; + /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 14',10,1) WITH NOWAIT; + END -/*Versioning details*/ - DECLARE @Version NVARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 14 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Log File Shrinking' AS Finding, + 'https://www.brentozar.com/askbrent/file-shrinking/' AS URL, + 'Number of shrinks 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, + 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND object_name = @ServiceName + ':Databases' + AND counter_name = 'Log Shrinks' + AND value_delta > 0; + /* Query Problems - Compilations/Sec High - CheckID 15 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 15',10,1) WITH NOWAIT; + END -IF @Help = 1 + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 15 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Compilations/Sec High' AS Finding, + 'https://www.brentozar.com/askbrent/compilations/' AS URL, + 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, + 'To find the queries that are compiling, start with:' + @LineFeed + + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed + + 'If dynamic SQL or non-parameterized strings are involved, consider enabling Forced Parameterization. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Compilations/sec' AND psComp.value_delta > 0 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec' + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 compilations per second, or more compilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Compilations are more than 10% of batch requests per second */ + /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ + IF (@Debug = 1) BEGIN - - PRINT ' - /* - sp_DatabaseRestore from http://FirstResponderKit.org - - This script will restore a database from a given file path. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - Tastes awful with marmite. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright for portions of sp_Blitz are held by Microsoft as part of project - tigertoolbox and are provided under the MIT license: - https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - */ - '; - - PRINT ' - /* - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 0, - @RunRecovery = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 1, - @RunRecovery = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 1, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 0, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1, - @TestRestore = 1, - @RunCheckDB = 1, - @Debug = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @StandbyMode = 1, - @StandbyUndoPath = ''D:\Data\'', - @ContinueLogs = 1, - @RunRecovery = 0, - @Debug = 0; - - --This example will restore the latest differential backup, and stop transaction logs at the specified date time. It will also only print the commands. - EXEC dbo.sp_DatabaseRestore - @Database = ''DBA'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1, - @StopAt = ''20170508201501'', - @Debug = 1; - - Variables: - - @RestoreDiff - This variable is a flag for whether or not the script is expecting to restore differentials - @StopAt - This variable is used to restore transaction logs to a specific date and time. The format must be in YYYYMMDDHHMMSS. The time is in military format. - - About Debug Modes: - - There are 3 Debug Modes. Mode 0 is the default and will execute the script. Debug 1 will print just the commands. Debug 2 will print other useful information that - has mostly been useful for troubleshooting. Debug 2 needs to be expanded to make it more useful. - */ - '; - - RETURN; - - END; + RAISERROR('Running CheckID 16',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 16 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Re-Compilations/Sec High' AS Finding, + 'https://www.brentozar.com/askbrent/recompilations/' AS URL, + 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, + 'To find the queries that are being forced to recompile, start with:' + @LineFeed + + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed + + 'Examine those plans to find out which objects are changing so quickly that they hit the stats update threshold. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Re-Compilations/sec' AND psComp.value_delta > 0 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec' + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 recompilations per second, or more recompilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Recompilations are more than 10% of batch requests per second */ + /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 29',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 29 AS CheckID, + 40 AS Priority, + 'Table Problems' AS FindingGroup, + 'Forwarded Fetches/Sec High' AS Finding, + 'https://www.brentozar.com/go/fetch/' AS URL, + CAST(ps.value_delta AS NVARCHAR(20)) + ' forwarded fetches (from SQLServer:Access Methods counter)' + @LineFeed + + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, + 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Access Methods' AND psComp.counter_name = 'Forwarded Records/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':Access Methods' + AND ps.counter_name = 'Forwarded Records/sec' + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ --- Get the SQL Server version number because the columns returned by RESTORE commands vary by version --- Based on: https://www.brentozar.com/archive/2015/05/sql-server-version-detection/ --- Need to capture BuildVersion because RESTORE HEADERONLY changed with 2014 CU1, not RTM -DECLARE @ProductVersion AS NVARCHAR(20) = CAST(SERVERPROPERTY ('productversion') AS NVARCHAR(20)); -DECLARE @MajorVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 4) AS SMALLINT); -DECLARE @MinorVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 3) AS SMALLINT); -DECLARE @BuildVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 2) AS SMALLINT); + /* Check for temp objects with high forwarded fetches. + This has to be done as dynamic SQL because we have to execute OBJECT_NAME inside TempDB. */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 29) + BEGIN + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT TOP 10 29 AS CheckID, + 40 AS Priority, + ''Table Problems'' AS FindingGroup, + ''Forwarded Fetches/Sec High: TempDB Object'' AS Finding, + ''https://www.brentozar.com/go/fetch/'' AS URL, + CAST(COALESCE(os.forwarded_fetch_count,0) - COALESCE(os_prior.forwarded_fetch_count,0) AS NVARCHAR(20)) + '' forwarded fetches on '' + + CASE WHEN OBJECT_NAME(os.object_id) IS NULL THEN ''an unknown table '' + WHEN LEN(OBJECT_NAME(os.object_id)) < 50 THEN ''a table variable, internal identifier '' + OBJECT_NAME(os.object_id) + ELSE ''a temp table '' + OBJECT_NAME(os.object_id) + END AS Details, + ''Look through your source code to find the object creating these objects, and tune the creation and population to reduce fetches. See the URL for details.'' AS HowToStopIt + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID(''tempdb''), NULL, NULL, NULL) os + LEFT OUTER JOIN #TempdbOperationalStats os_prior ON os.object_id = os_prior.object_id + AND os.forwarded_fetch_count > os_prior.forwarded_fetch_count + WHERE os.database_id = DB_ID(''tempdb'') + AND os.forwarded_fetch_count - COALESCE(os_prior.forwarded_fetch_count,0) > 100 + ORDER BY os.forwarded_fetch_count DESC;' -IF @MajorVersion < 10 -BEGIN - RAISERROR('Sorry, DatabaseRestore doesn''t work on versions of SQL prior to 2008.', 15, 1); - RETURN; -END; + EXECUTE sp_executesql @StringToExecute; + END + /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 31',10,1) WITH NOWAIT; + END -DECLARE @cmd NVARCHAR(4000) = N'', --Holds xp_cmdshell command - @sql NVARCHAR(MAX) = N'', --Holds executable SQL commands - @LastFullBackup NVARCHAR(500) = N'', --Last full backup name - @LastDiffBackup NVARCHAR(500) = N'', --Last diff backup name - @LastDiffBackupDateTime NVARCHAR(500) = N'', --Last diff backup date - @BackupFile NVARCHAR(500) = N'', --Name of backup file - @BackupDateTime AS CHAR(15) = N'', --Used for comparisons to generate ordered backup files/create a stopat point - @FullLastLSN NUMERIC(25, 0), --LSN for full - @DiffLastLSN NUMERIC(25, 0), --LSN for diff - @HeadersSQL AS NVARCHAR(4000) = N'', --Dynamic insert into #Headers table (deals with varying results from RESTORE FILELISTONLY across different versions) - @MoveOption AS NVARCHAR(MAX) = N'', --If you need to move restored files to a different directory - @LogRecoveryOption AS NVARCHAR(MAX) = N'', --Holds the option to cause logs to be restored in standby mode or with no recovery - @DatabaseLastLSN NUMERIC(25, 0), --redo_start_lsn of the current database - @i TINYINT = 1, --Maintains loop to continue logs - @LogFirstLSN NUMERIC(25, 0), --Holds first LSN in log backup headers - @LogLastLSN NUMERIC(25, 0), --Holds last LSN in log backup headers - @FileListParamSQL NVARCHAR(4000) = N''; --Holds INSERT list for #FileListParameters + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 31 AS CheckID, + 50 AS Priority, + 'In-Memory OLTP' AS FindingGroup, + 'Garbage Collection in Progress' AS Finding, + 'https://www.brentozar.com/go/garbage/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed + + 'This can happen for a few reasons: ' + @LineFeed + + 'Memory-Optimized TempDB, or ' + @LineFeed + + 'transactional workloads that constantly insert/delete data in In-Memory OLTP tables, or ' + @LineFeed + + 'memory pressure (causing In-Memory OLTP to shrink its footprint) or' AS Details, + 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name LIKE '%XTP Garbage Collection' + AND ps.counter_name = 'Rows processed/sec' + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ -DECLARE @FileList TABLE -( - BackupFile NVARCHAR(255) -); + /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 32',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 32 AS CheckID, + 100 AS Priority, + 'In-Memory OLTP' AS FindingGroup, + 'Transactions Aborted' AS Finding, + 'https://www.brentozar.com/go/aborted/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed + + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, + 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Transactions' AND psComp.counter_name = 'Transactions aborted/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name LIKE '%XTP Transactions' + AND ps.counter_name = 'Transactions aborted/sec' + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ -IF OBJECT_ID(N'tempdb..#FileListParameters') IS NOT NULL DROP TABLE #FileListParameters; -CREATE TABLE #FileListParameters -( - LogicalName NVARCHAR(128) NOT NULL, - PhysicalName NVARCHAR(260) NOT NULL, - Type CHAR(1) NOT NULL, - FileGroupName NVARCHAR(120) NULL, - Size NUMERIC(20, 0) NOT NULL, - MaxSize NUMERIC(20, 0) NOT NULL, - FileID BIGINT NULL, - CreateLSN NUMERIC(25, 0) NULL, - DropLSN NUMERIC(25, 0) NULL, - UniqueID UNIQUEIDENTIFIER NULL, - ReadOnlyLSN NUMERIC(25, 0) NULL, - ReadWriteLSN NUMERIC(25, 0) NULL, - BackupSizeInBytes BIGINT NULL, - SourceBlockSize INT NULL, - FileGroupID INT NULL, - LogGroupGUID UNIQUEIDENTIFIER NULL, - DifferentialBaseLSN NUMERIC(25, 0) NULL, - DifferentialBaseGUID UNIQUEIDENTIFIER NULL, - IsReadOnly BIT NULL, - IsPresent BIT NULL, - TDEThumbprint VARBINARY(32) NULL, - SnapshotUrl NVARCHAR(360) NULL -); + /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 33',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 32 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Suboptimal Plans/Sec High' AS Finding, + 'https://www.brentozar.com/go/suboptimal/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed + + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, + 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Workload GroupStats' AND psComp.counter_name = 'Suboptimal plans/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':Workload GroupStats' + AND ps.counter_name = 'Suboptimal plans/sec' + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ + /* Azure Performance - Database is Maxed Out - CheckID 41 */ + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 41',10,1) WITH NOWAIT; + END -IF OBJECT_ID(N'tempdb..#Headers') IS NOT NULL DROP TABLE #Headers; -CREATE TABLE #Headers -( - BackupName NVARCHAR(256), - BackupDescription NVARCHAR(256), - BackupType NVARCHAR(256), - ExpirationDate NVARCHAR(256), - Compressed NVARCHAR(256), - Position NVARCHAR(256), - DeviceType NVARCHAR(256), - UserName NVARCHAR(256), - ServerName NVARCHAR(256), - DatabaseName NVARCHAR(256), - DatabaseVersion NVARCHAR(256), - DatabaseCreationDate NVARCHAR(256), - BackupSize NVARCHAR(256), - FirstLSN NVARCHAR(256), - LastLSN NVARCHAR(256), - CheckpointLSN NVARCHAR(256), - DatabaseBackupLSN NVARCHAR(256), - BackupStartDate NVARCHAR(256), - BackupFinishDate NVARCHAR(256), - SortOrder NVARCHAR(256), - CodePage NVARCHAR(256), - UnicodeLocaleId NVARCHAR(256), - UnicodeComparisonStyle NVARCHAR(256), - CompatibilityLevel NVARCHAR(256), - SoftwareVendorId NVARCHAR(256), - SoftwareVersionMajor NVARCHAR(256), - SoftwareVersionMinor NVARCHAR(256), - SoftwareVersionBuild NVARCHAR(256), - MachineName NVARCHAR(256), - Flags NVARCHAR(256), - BindingID NVARCHAR(256), - RecoveryForkID NVARCHAR(256), - Collation NVARCHAR(256), - FamilyGUID NVARCHAR(256), - HasBulkLoggedData NVARCHAR(256), - IsSnapshot NVARCHAR(256), - IsReadOnly NVARCHAR(256), - IsSingleUser NVARCHAR(256), - HasBackupChecksums NVARCHAR(256), - IsDamaged NVARCHAR(256), - BeginsLogChain NVARCHAR(256), - HasIncompleteMetaData NVARCHAR(256), - IsForceOffline NVARCHAR(256), - IsCopyOnly NVARCHAR(256), - FirstRecoveryForkID NVARCHAR(256), - ForkPointLSN NVARCHAR(256), - RecoveryModel NVARCHAR(256), - DifferentialBaseLSN NVARCHAR(256), - DifferentialBaseGUID NVARCHAR(256), - BackupTypeDescription NVARCHAR(256), - BackupSetGUID NVARCHAR(256), - CompressedBackupSize NVARCHAR(256), - Containment NVARCHAR(256), - KeyAlgorithm NVARCHAR(32), - EncryptorThumbprint VARBINARY(20), - EncryptorType NVARCHAR(32), - -- - -- Seq added to retain order by - -- - Seq INT NOT NULL IDENTITY(1, 1) -); + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 41 AS CheckID, + 10 AS Priority, + 'Azure Performance' AS FindingGroup, + 'Database is Maxed Out' AS Finding, + 'https://www.brentozar.com/go/maxedout' AS URL, + N'At ' + CONVERT(NVARCHAR(100), s.end_time ,121) + N', your database approached (or hit) your DTU limits:' + @LineFeed + + N'Average CPU percent: ' + CAST(avg_cpu_percent AS NVARCHAR(50)) + @LineFeed + + N'Average data IO percent: ' + CAST(avg_data_io_percent AS NVARCHAR(50)) + @LineFeed + + N'Average log write percent: ' + CAST(avg_log_write_percent AS NVARCHAR(50)) + @LineFeed + + N'Max worker percent: ' + CAST(max_worker_percent AS NVARCHAR(50)) + @LineFeed + + N'Max session percent: ' + CAST(max_session_percent AS NVARCHAR(50)) AS Details, + 'Tune your queries or indexes with sp_BlitzCache or sp_BlitzIndex, or consider upgrading to a higher DTU level.' AS HowToStopIt + FROM sys.dm_db_resource_stats s + WHERE s.end_time >= DATEADD(MI, -5, GETDATE()) + AND (avg_cpu_percent > 90 + OR avg_data_io_percent >= 90 + OR avg_log_write_percent >=90 + OR max_worker_percent >= 90 + 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 -Correct paths in case people forget a final "\" + ;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 + RAISERROR('Running CheckID 19',10,1) WITH NOWAIT; + END -/*Full*/ + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 19 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'Batch Requests per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(CAST(ps.value_delta AS MONEY) / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec'; + + + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); + + /* Server Info - SQL Compilations/sec - CheckID 25 */ + IF @ExpertMode >= 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 25',10,1) WITH NOWAIT; + END -IF (SELECT RIGHT(@BackupPathFull, 1)) <> '\' --Has to end in a '\' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 25 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Compilations/sec'; + END + + /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ + IF @ExpertMode >= 1 BEGIN - RAISERROR('Fixing @BackupPathFull to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathFull += N'\'; - END; + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 26',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 26 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Re-Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Re-Compilations/sec'; + END + + /* Server Info - Wait Time per Core per Sec - CheckID 20 */ + IF @Seconds > 0 + BEGIN; + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 20',10,1) WITH NOWAIT; + END; + + WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), + waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), + cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 20 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'Wait Time per Core per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST((CAST(waits2.waits_ms - waits1.waits_ms AS MONEY)) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS NVARCHAR(20)) AS Details, + (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS DetailsInt + FROM cores i + CROSS JOIN waits1 + CROSS JOIN waits2; + END; -/*Diff*/ -IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '\' --Has to end in a '\' + IF @Seconds > 0 BEGIN - RAISERROR('Fixing @BackupPathDiff to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathDiff += N'\'; - END; + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 2 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END -/*Log*/ -IF (SELECT RIGHT(@BackupPathLog, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @BackupPathLog to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathLog += N'\'; - END; + /* If we're waiting 30+ seconds, run these checks at the end. + We get this data from the ring buffers, and it's only updated once per minute, so might + as well get it now - whereas if we're checking 30+ seconds, it might get updated by the + end of our sp_BlitzFirst session. */ + IF @Seconds >= 30 + BEGIN + /* Server Performance - High CPU Utilization CheckID 24 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; + END -/*Move Data File*/ -IF NULLIF(@MoveDataDrive, '') IS NULL - BEGIN - RAISERROR('Getting default data drive for @MoveDataDrive', 0, 1) WITH NOWAIT; - SET @MoveDataDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); - END; -IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; - SET @MoveDataDrive += N'\'; - 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' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; -/*Move Log File*/ -IF NULLIF(@MoveLogDrive, '') IS NULL - BEGIN - RAISERROR('Getting default log drive for @@MoveLogDrive', 0, 1) WITH NOWAIT; - SET @MoveLogDrive = CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(260)); - END; -IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; - SET @MoveLogDrive += N'\'; - END; + /* Server Performance - CPU Utilization CheckID 23 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + END -/*Standby Undo File*/ -IF (SELECT RIGHT(@StandbyUndoPath, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @StandbyUndoPath to add a "\"', 0, 1) WITH NOWAIT; - SET @StandbyUndoPath += N'\'; - 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' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; + END; /* IF @Seconds >= 30 */ -IF @RestoreDatabaseName IS NULL - BEGIN - SET @RestoreDatabaseName = QUOTENAME(@Database); - END -ELSE + IF /* Let people on <2016 know about the thread time column */ + ( + @Seconds > 0 + AND @total_cpu_usage = 0 + ) BEGIN - SET @RestoreDatabaseName = QUOTENAME(@RestoreDatabaseName) - END + INSERT INTO + #BlitzFirstResults + ( + CheckID, + Priority, + FindingsGroup, + Finding, + Details, + URL + ) + SELECT + 48, + 254, + N'Informational', + N'Thread Time comes from the plan cache in versions earlier than 2016, and is not as reliable', + N'The oldest plan in your cache is from ' + + CONVERT(nvarchar(30), MIN(s.creation_time)) + + N' and your server was last restarted on ' + + CONVERT(nvarchar(30), MAX(o.sqlserver_start_time)), + N'https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-schedulers-transact-sql' + FROM sys.dm_exec_query_stats AS s + CROSS JOIN sys.dm_os_sys_info AS o + OPTION(RECOMPILE); + END /* Let people on <2016 know about the thread time column */ + /* If we didn't find anything, apologize. */ + IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) + BEGIN -IF @BackupPathFull IS NOT NULL + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 1 , + 'No Problems Found' , + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' + ); -BEGIN + END; /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ --- Get list of files -SET @cmd = N'DIR /b "' + @BackupPathFull + N'"'; + /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 255 , + 'Thanks!' , + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' + ); - IF @Debug = 1 - BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathFull'; - PRINT @cmd; - END; + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 0 , + 'sp_BlitzFirst ' + CAST(CONVERT(DATETIMEOFFSET, @VersionDate, 102) AS VARCHAR(100)), + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'We hope you found this tool useful.' + ); -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; + /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old - CheckID 27 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 27',10,1) WITH NOWAIT; + END -/*Sanity check folders*/ + IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 + BEGIN + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 27 AS CheckID , + 0 AS Priority , + 'Outdated sp_BlitzFirst' AS FindingsGroup , + 'sp_BlitzFirst is Over 6 Months Old' AS Finding , + 'http://FirstResponderKit.org/' AS URL , + 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details; + END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 + IF @CheckServerInfo = 0 /* Github #1680 */ + BEGIN + DELETE #BlitzFirstResults + WHERE FindingsGroup = 'Server Info'; + END - BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; + RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; - END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 + /* If they want to run sp_BlitzCache and export to table, go for it. */ + IF @OutputTableNameBlitzCache IS NOT NULL + AND @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN - BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathFull) WITH NOWAIT; - END; + RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 - BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END + /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ + IF EXISTS (SELECT * FROM sys.objects o + INNER JOIN sys.parameters pMB ON o.object_id = pMB.object_id AND pMB.name = '@MinutesBack' + INNER JOIN sys.parameters pCDO ON o.object_id = pCDO.object_id AND pCDO.name = '@CheckDateOverride' + WHERE o.name = 'sp_BlitzCache') + BEGIN + /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + QUOTENAME(@OutputTableNameBlitzCache) + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + QUOTENAME(@OutputTableNameBlitzCache) + + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; + EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The user name or password is incorrect.' - ) = 1 - - BEGIN - - RAISERROR('Incorrect user name or password for %s', 16, 1, @BackupPathFull) WITH NOWAIT; - - END; + /* If there's no data, let's just analyze the last 15 minutes of the plan cache */ + IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 + SET @BlitzCacheMinutesBack = 15; -/*End folder sanity check*/ + IF(@OutputType = 'NONE') + BEGIN + EXEC sp_BlitzCache + @OutputDatabaseName = @UnquotedOutputDatabaseName, + @OutputSchemaName = @UnquotedOutputSchemaName, + @OutputTableName = @OutputTableNameBlitzCache, + @CheckDateOverride = @StartSampleTime, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, + @MinutesBack = @BlitzCacheMinutesBack, + @Debug = @Debug, + @OutputType = @OutputType + ; + END; + ELSE + BEGIN + + EXEC sp_BlitzCache + @OutputDatabaseName = @UnquotedOutputDatabaseName, + @OutputSchemaName = @UnquotedOutputSchemaName, + @OutputTableName = @OutputTableNameBlitzCache, + @CheckDateOverride = @StartSampleTime, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, + @MinutesBack = @BlitzCacheMinutesBack, + @Debug = @Debug + ; + END; --- Find latest full backup -SELECT @LastFullBackup = MAX(BackupFile) -FROM @FileList -WHERE BackupFile LIKE N'%.bak' - AND - BackupFile LIKE N'%' + @Database + N'%' - AND - (@StopAt IS NULL OR REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt); + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + QUOTENAME(@OutputTableNameBlitzCache) + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - IF @Debug = 1 - BEGIN - SELECT * - FROM @FileList; - END; + END; + ELSE + BEGIN + /* No sp_BlitzCache found, or it's outdated - CheckID 36 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 36',10,1) WITH NOWAIT; + END -SET @FileListParamSQL = - N'INSERT INTO #FileListParameters WITH (TABLOCK) - (LogicalName, PhysicalName, Type, FileGroupName, Size, MaxSize, FileID, CreateLSN, DropLSN - ,UniqueID, ReadOnlyLSN, ReadWriteLSN, BackupSizeInBytes, SourceBlockSize, FileGroupID, LogGroupGUID - ,DifferentialBaseLSN, DifferentialBaseGUID, IsReadOnly, IsPresent, TDEThumbprint'; + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 36 AS CheckID , + 0 AS Priority , + 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , + 'Update Your sp_BlitzCache' AS Finding , + 'http://FirstResponderKit.org/' AS URL , + 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details; + END; -IF @MajorVersion >= 13 - BEGIN - SET @FileListParamSQL += N', SnapshotUrl'; - END; + RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; -SET @FileListParamSQL += N')' + NCHAR(13) + NCHAR(10); -SET @FileListParamSQL += N'EXEC (''RESTORE FILELISTONLY FROM DISK=''''{Path}'''''')'; + END; /* End running sp_BlitzCache */ -SET @sql = REPLACE(@FileListParamSQL, N'{Path}', @BackupPathFull + @LastFullBackup); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for INSERT to #FileListParameters: @BackupPathFull + @LastFullBackup'; - PRINT @sql; - END; + /* @OutputTableName lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND @OutputTableName NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NOT NULL, + Details NVARCHAR(4000) NULL, + HowToStopIt [XML] NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; -EXEC (@sql); + EXEC(@StringToExecute); - IF @Debug = 1 - BEGIN - SELECT '#FileListParameters' AS table_name, * FROM #FileListParameters - END + /* If the table doesn't have the new QueryHash column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''QueryHash'') + ALTER TABLE ' + @ObjectFullName + N' ADD QueryHash BINARY(8) NULL;'; + EXEC(@StringToExecute); + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2164. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); -SET @HeadersSQL = -N'INSERT INTO #Headers WITH (TABLOCK) - (BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, Position, DeviceType, UserName, ServerName - ,DatabaseName, DatabaseVersion, DatabaseCreationDate, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN - ,BackupStartDate, BackupFinishDate, SortOrder, CodePage, UnicodeLocaleId, UnicodeComparisonStyle, CompatibilityLevel - ,SoftwareVendorId, SoftwareVersionMajor, SoftwareVersionMinor, SoftwareVersionBuild, MachineName, Flags, BindingID - ,RecoveryForkID, Collation, FamilyGUID, HasBulkLoggedData, IsSnapshot, IsReadOnly, IsSingleUser, HasBackupChecksums - ,IsDamaged, BeginsLogChain, HasIncompleteMetaData, IsForceOffline, IsCopyOnly, FirstRecoveryForkID, ForkPointLSN - ,RecoveryModel, DifferentialBaseLSN, DifferentialBaseGUID, BackupTypeDescription, BackupSetGUID, CompressedBackupSize'; - -IF @MajorVersion >= 11 - SET @HeadersSQL += NCHAR(13) + NCHAR(10) + N', Containment'; + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; -IF @MajorVersion >= 13 OR (@MajorVersion = 12 AND @BuildVersion >= 2342) - SET @HeadersSQL += N', KeyAlgorithm, EncryptorThumbprint, EncryptorType'; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NULL) CREATE TABLE ' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NOT NULL, + Details NVARCHAR(4000) NULL, + HowToStopIt [XML] NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; -SET @HeadersSQL += N')' + NCHAR(13) + NCHAR(10); -SET @HeadersSQL += N'EXEC (''RESTORE HEADERONLY FROM DISK=''''{Path}'''''')'; + /* @OutputTableNameFileStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNameFileStats IS NOT NULL + AND @OutputTableNameFileStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNameFileStats + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + PRIMARY KEY CLUSTERED (ID ASC));'; + EXEC(@StringToExecute); -IF @MoveFiles = 1 - BEGIN - - RAISERROR('@MoveFiles = 1, adjusting paths', 0, 1) WITH NOWAIT; - - WITH Files - AS ( - SELECT N', MOVE ''' + LogicalName + N''' TO ''' + - CASE - WHEN Type = 'D' THEN @MoveDataDrive - WHEN Type = 'L' THEN @MoveLogDrive - END + CASE WHEN @Database = @RestoreDatabaseName THEN REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)) + '''' - ELSE REPLACE(REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)), @Database, SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) -2)) + '''' - END AS logicalcmds - FROM #FileListParameters) - - SELECT @MoveOption = @MoveOption + Files.logicalcmds - FROM Files; - - IF @Debug = 1 PRINT @MoveOption + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; - END; + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View + N';'; + EXEC(@StringToExecute); + END -IF @ContinueLogs = 0 - BEGIN + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + ' SELECT f.ServerName,' + @LineFeed + + ' f.CheckDate,' + @LineFeed + + ' f.DatabaseID,' + @LineFeed + + ' f.DatabaseName,' + @LineFeed + + ' f.FileID,' + @LineFeed + + ' f.FileLogicalName,' + @LineFeed + + ' f.TypeDesc,' + @LineFeed + + ' f.PhysicalName,' + @LineFeed + + ' f.SizeOnDiskMB,' + @LineFeed + + ' DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds,' + @LineFeed + + ' (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth,' + @LineFeed + + ' (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms,' + @LineFeed + + ' io_stall_read_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_reads - fPrior.num_of_reads) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads,' + @LineFeed + + ' (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read,' + @LineFeed + + ' (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms,' + @LineFeed + + ' io_stall_write_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_writes - fPrior.num_of_writes) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes,' + @LineFeed + + ' (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written, ' + @LineFeed + + ' f.ServerName + CAST(f.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed + + ' INNER HASH JOIN CheckDates DATES ON f.CheckDate = DATES.CheckDate' + @LineFeed + + ' INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName' + @LineFeed + + ' AND f.DatabaseID = fPrior.DatabaseID' + @LineFeed + + ' AND f.FileID = fPrior.FileID' + @LineFeed + + ' AND fPrior.CheckDate = DATES.PreviousCheckDate' + @LineFeed + + '' + @LineFeed + + ' WHERE f.num_of_reads >= fPrior.num_of_reads' + @LineFeed + + ' AND f.num_of_writes >= fPrior.num_of_writes' + @LineFeed + + ' AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END; - RAISERROR('@ContinueLogs set to 0', 0, 1) WITH NOWAIT; - - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathFull + @LastFullBackup + N''' WITH NORECOVERY, REPLACE' + @MoveOption + NCHAR(13); - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathFull, @LastFullBackup, @MoveOption'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; - - --get the backup completed data so we can apply tlogs from that point forwards - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathFull + @LastFullBackup); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for get backup completed data: @BackupPathFull, @LastFullBackup'; - PRINT @sql; - END; - - EXECUTE (@sql); - - --setting the @BackupDateTime to a numeric string so that it can be used in comparisons - SET @BackupDateTime = REPLACE(LEFT(RIGHT(@LastFullBackup, 19),15), '_', ''); - - SELECT @FullLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) FROM #Headers WHERE BackupType = 1; + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - IF @Debug = 1 - BEGIN - IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastFullBackup'; - PRINT @BackupDateTime; - END; - - END; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; -ELSE + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; - BEGIN - - SELECT @DatabaseLastLSN = CAST(f.redo_start_lsn AS NUMERIC(25, 0)) - FROM master.sys.databases d - JOIN master.sys.master_files f ON d.database_id = f.database_id - WHERE d.name = SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) - 2) AND f.file_id = 1; - - END; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; --- Clear out table variables for differential -DELETE FROM @FileList; + END; + ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNameFileStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNameFileStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + DetailsInt INT NULL, + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNameFileStats + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; -END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; -IF @BackupPathDiff IS NOT NULL + /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNamePerfmonStats IS NOT NULL + AND @OutputTableNamePerfmonStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNamePerfmonStats + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL, + PRIMARY KEY CLUSTERED (ID ASC));'; -BEGIN + EXEC(@StringToExecute); --- Get list of files -SET @cmd = N'DIR /b "'+ @BackupPathDiff + N'"'; + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; - IF @Debug = 1 - BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathDiff check'; - PRINT @cmd; - END; + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View + N';'; - IF @Debug = 1 - BEGIN - SELECT * - FROM @FileList; - END; + EXEC(@StringToExecute); + END + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + 'SELECT' + @LineFeed + + ' pMon.[ServerName]' + @LineFeed + + ' ,pMon.[CheckDate]' + @LineFeed + + ' ,pMon.[object_name]' + @LineFeed + + ' ,pMon.[counter_name]' + @LineFeed + + ' ,pMon.[instance_name]' + @LineFeed + + ' ,DATEDIFF(SECOND,pMonPrior.[CheckDate],pMon.[CheckDate]) AS ElapsedSeconds' + @LineFeed + + ' ,pMon.[cntr_value]' + @LineFeed + + ' ,pMon.[cntr_type]' + @LineFeed + + ' ,(pMon.[cntr_value] - pMonPrior.[cntr_value]) AS cntr_delta' + @LineFeed + + ' ,(pMon.cntr_value - pMonPrior.cntr_value) * 1.0 / DATEDIFF(ss, pMonPrior.CheckDate, pMon.CheckDate) AS cntr_delta_per_second' + @LineFeed + + ' ,pMon.ServerName + CAST(pMon.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMon' + @LineFeed + + ' INNER HASH JOIN CheckDates Dates' + @LineFeed + + ' ON Dates.CheckDate = pMon.CheckDate' + @LineFeed + + ' JOIN ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMonPrior' + @LineFeed + + ' ON Dates.PreviousCheckDate = pMonPrior.CheckDate' + @LineFeed + + ' AND pMon.[ServerName] = pMonPrior.[ServerName] ' + @LineFeed + + ' AND pMon.[object_name] = pMonPrior.[object_name] ' + @LineFeed + + ' AND pMon.[counter_name] = pMonPrior.[counter_name] ' + @LineFeed + + ' AND pMon.[instance_name] = pMonPrior.[instance_name]' + @LineFeed + + ' WHERE DATEDIFF(MI, pMonPrior.CheckDate, pMon.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View; -/*Sanity check folders*/ + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View + N';'; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 + EXEC(@StringToExecute); + END - BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; + /* Create the second view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStatsActuals_View + ' AS ' + @LineFeed + + 'WITH PERF_AVERAGE_BULK AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CASE WHEN CHARINDEX(''''('''', counter_name) = 0 THEN counter_name ELSE LEFT (counter_name, CHARINDEX(''''('''',counter_name)-1) END AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + @LineFeed + + ' WHERE cntr_type IN(1073874176)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_LARGE_RAW_BASE AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' LEFT(counter_name, CHARINDEX(''''BASE'''', UPPER(counter_name))-1) AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(1073939712)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_AVERAGE_FRACTION AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' counter_name AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(537003264)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_BULK_COUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta / ElapsedSeconds AS cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(272696576, 272696320)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_RAWCOUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(65792, 65536)' + @LineFeed + + ')' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' NUM.cntr_delta / DEN.cntr_delta AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' ' + @LineFeed + + 'FROM PERF_AVERAGE_BULK AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' CAST((CAST(NUM.cntr_delta as DECIMAL(19)) / DEN.cntr_delta) as decimal(23,3)) AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_AVERAGE_FRACTION AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_BULK_COUNT' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_RAWCOUNT;'')'; + + EXEC(@StringToExecute); + END; - END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathDiff) WITH NOWAIT; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; - END; + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END -/*End folder sanity check*/ + END; + ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNamePerfmonStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNamePerfmonStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL, + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNamePerfmonStats + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; --- Find latest diff backup -SELECT @LastDiffBackup = MAX(BackupFile) -FROM @FileList -WHERE BackupFile LIKE N'%.bak' - AND - BackupFile LIKE N'%' + @Database + '%' - AND - (@StopAt IS NULL OR REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt); - - --set the @BackupDateTime so that it can be used for comparisons - SET @BackupDateTime = REPLACE(@BackupDateTime, '_', ''); - SET @LastDiffBackupDateTime = REPLACE(LEFT(RIGHT(@LastDiffBackup, 19),15), '_', ''); + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; -IF @RestoreDiff = 1 AND @BackupDateTime < @LastDiffBackupDateTime - BEGIN - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH NORECOVERY' + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathDiff, @LastDiffBackup'; - PRINT @sql; - END; + /* @OutputTableNameWaitStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNameWaitStats IS NOT NULL + AND @OutputTableNameWaitStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNameWaitStats + ''') ' + @LineFeed + + 'BEGIN' + @LineFeed + + 'CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT , + PRIMARY KEY CLUSTERED (ID));' + @LineFeed + + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed + + 'END'; - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; - - --get the backup completed data so we can apply tlogs from that point forwards - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathDiff + @LastDiffBackup); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @BackupPathDiff, @LastDiffBackup'; - PRINT @sql; - END; - - EXECUTE (@sql); + EXEC(@StringToExecute); - IF @Debug = 1 - BEGIN - SELECT '#Headers' AS table_name, * FROM #Headers AS h - END - - --set the @BackupDateTime to the date time on the most recent differential - SET @BackupDateTime = @LastDiffBackupDateTime; - - SELECT @DiffLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) - FROM #Headers - WHERE BackupType = 5; - END; + /* Create the wait stats category table */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_Categories; + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')'; --- Clear out table variables for translogs -DELETE FROM @FileList; - - END + EXEC(@StringToExecute); + END; - IF @BackupPathLog IS NOT NULL + /* Make sure the wait stats category table has the current number of rows */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''IF (SELECT COALESCE(SUM(1),0) FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ') <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories)' + @LineFeed + + 'BEGIN ' + @LineFeed + + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed + + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed + + 'END'')'; -BEGIN + EXEC(@StringToExecute); -SET @cmd = N'DIR /b "' + @BackupPathLog + N'"'; - IF @Debug = 1 - BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathLog check'; - PRINT @cmd; - END; + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; - IF @Debug = 1 - BEGIN - SELECT * - FROM @FileList; - END; + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View + N';'; + EXEC(@StringToExecute); + END -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; -/*Sanity check folders*/ + /* Create the wait stats view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed + + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) / 60000.0 AS wait_time_minutes_delta' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed + + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed + + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed + + ', w.ServerName + CAST(w.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed + + 'INNER HASH JOIN CheckDates Dates' + @LineFeed + + 'ON Dates.CheckDate = w.CheckDate' + @LineFeed + + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND Dates.PreviousCheckDate = wPrior.CheckDate' + @LineFeed + + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed + + 'WHERE DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60' + @LineFeed + + 'AND [w].[wait_time_ms] >= [wPrior].[wait_time_ms];'')' + + EXEC(@StringToExecute); + END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 - BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - END; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; - BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathLog) WITH NOWAIT; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - END; + END; + ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNameWaitStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNameWaitStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT , + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNameWaitStats + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; - BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END -/*End folder sanity check*/ -IF (@OnlyLogsAfter IS NOT NULL) - BEGIN - - RAISERROR('@OnlyLogsAfter is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; - - DELETE fl - FROM @FileList AS fl - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND REPLACE(LEFT(RIGHT(fl.BackupFile, 19), 15),'_','') < @OnlyLogsAfter - -END + DECLARE @separator AS VARCHAR(1); + IF @OutputType = 'RSV' + SET @separator = CHAR(31); + ELSE + SET @separator = ','; + IF @OutputType = 'COUNT' AND @SinceStartup = 0 + BEGIN + SELECT COUNT(*) AS Warnings + FROM #BlitzFirstResults; + END; + ELSE + IF @OutputType = 'Opserver1' AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN --- Check for log backups -IF(@StopAt IS NULL AND @OnlyLogsAfter IS NULL) - BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @BackupDateTime)) - ORDER BY BackupFile; - - OPEN BackupFiles; - END; + SELECT r.[Priority] , + r.[FindingsGroup] , + r.[Finding] , + r.[URL] , + r.[Details], + r.[HowToStopIt] , + r.[CheckID] , + r.[StartTime], + r.[LoginName], + r.[NTUserName], + r.[OriginalLoginName], + r.[ProgramName], + r.[HostName], + r.[DatabaseID], + r.[DatabaseName], + r.[OpenTransactionCount], + r.[QueryPlan], + r.[QueryText], + qsNow.plan_handle AS PlanHandle, + qsNow.sql_handle AS SqlHandle, + qsNow.statement_start_offset AS StatementStartOffset, + qsNow.statement_end_offset AS StatementEndOffset, + [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), + [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), + [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), + [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), + [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), + [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), + [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), + [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), + [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), + [TotalExecutions] = qsNow.execution_count, + [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), + [TotalDuration] = qsNow.total_elapsed_time, + [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), + [TotalCPU] = qsNow.total_worker_time, + [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), + [TotalReads] = qsNow.total_logical_reads, + [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), + r.[DetailsInt] + FROM #BlitzFirstResults r + LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 + LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID + LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID + ORDER BY r.Priority , + r.FindingsGroup , + CASE + WHEN r.CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + r.Finding, + r.ID; + END; + ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + + SELECT Result = CAST([Priority] AS NVARCHAR(100)) + + @separator + CAST(CheckID AS NVARCHAR(100)) + + @separator + COALESCE([FindingsGroup], + '(N/A)') + @separator + + COALESCE([Finding], '(N/A)') + @separator + + COALESCE(DatabaseName, '(N/A)') + @separator + + COALESCE([URL], '(N/A)') + @separator + + COALESCE([Details], '(N/A)') + FROM #BlitzFirstResults + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + Details; + END; + ELSE IF @OutputType = 'Top10' AND @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in hours */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT TOP 10 + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], + 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], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] + 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + ELSE IF @ExpertMode = 0 AND @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS XML) AS Details, + CAST(@StockWarningHeader + HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, + [QueryText], + [QueryPlan] + FROM #BlitzFirstResults + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + CAST(LEFT(@StockDetailsHeader + [Details] + @StockDetailsFooter,32000) AS TEXT) AS Details, + CAST(LEFT([HowToStopIt],32000) AS TEXT) AS HowToStopIt, + CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, + CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan + FROM #BlitzFirstResults + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @ExpertMode >= 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%Findings%' + BEGIN + IF @SinceStartup = 0 + SELECT r.[Priority] , + r.[FindingsGroup] , + r.[Finding] , + r.[URL] , + CAST(@StockDetailsHeader + r.[Details] + @StockDetailsFooter AS XML) AS Details, + CAST(@StockWarningHeader + r.HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, + r.[CheckID] , + r.[StartTime], + r.[LoginName], + r.[NTUserName], + r.[OriginalLoginName], + r.[ProgramName], + r.[HostName], + r.[DatabaseID], + r.[DatabaseName], + r.[OpenTransactionCount], + r.[QueryPlan], + r.[QueryText], + qsNow.plan_handle AS PlanHandle, + qsNow.sql_handle AS SqlHandle, + qsNow.statement_start_offset AS StatementStartOffset, + qsNow.statement_end_offset AS StatementEndOffset, + [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), + [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), + [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), + [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), + [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), + [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), + [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), + [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), + [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), + [TotalExecutions] = qsNow.execution_count, + [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), + [TotalDuration] = qsNow.total_elapsed_time, + [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), + [TotalCPU] = qsNow.total_worker_time, + [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), + [TotalReads] = qsNow.total_logical_reads, + [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), + r.[DetailsInt] + FROM #BlitzFirstResults r + LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 + LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID + LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY r.Priority , + r.FindingsGroup , + CASE + WHEN r.CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + r.Finding, + r.ID, + CAST(r.Details AS NVARCHAR(4000)); + ------------------------- + --What happened: #WaitStats + ------------------------- + IF @Seconds = 0 AND @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in hours */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT + 'WAIT STATS' AS Pattern, + b.SampleTime AS [Sample Ended], + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60. AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + ELSE IF @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in seconds */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT + 'WAIT STATS' AS Pattern, + b.SampleTime AS [Sample Ended], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], + c.[Total Thread Time (Seconds)], + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms - wd1.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; -IF (@StopAt IS NULL AND @OnlyLogsAfter IS NOT NULL) - BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @OnlyLogsAfter)) - ORDER BY BackupFile; - - OPEN BackupFiles; - END + ------------------------- + --What happened: #FileStats + ------------------------- + IF @OutputResultSets LIKE N'%FileStats%' + WITH readstats AS ( + SELECT 'PHYSICAL READS' AS Pattern, + ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, + wd2.SampleTime AS [Sample Time], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], + wd1.DatabaseName , + wd1.FileLogicalName AS [File Name], + UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , + wd1.SizeOnDiskMB , + ( wd2.num_of_reads - wd1.num_of_reads ) AS [# Reads/Writes], + CASE WHEN wd2.num_of_reads - wd1.num_of_reads > 0 + THEN CAST(( wd2.bytes_read - wd1.bytes_read)/1024./1024. AS NUMERIC(21,1)) + ELSE 0 + END AS [MB Read/Written], + wd2.avg_stall_read_ms AS [Avg Stall (ms)], + wd1.PhysicalName AS [file physical name] + FROM #FileStats wd2 + JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime + AND wd1.DatabaseID = wd2.DatabaseID + AND wd1.FileID = wd2.FileID + ), + writestats AS ( + SELECT + 'PHYSICAL WRITES' AS Pattern, + ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_write_ms DESC) AS StallRank, + wd2.SampleTime AS [Sample Time], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], + wd1.DatabaseName , + wd1.FileLogicalName AS [File Name], + UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , + wd1.SizeOnDiskMB , + ( wd2.num_of_writes - wd1.num_of_writes ) AS [# Reads/Writes], + CASE WHEN wd2.num_of_writes - wd1.num_of_writes > 0 + THEN CAST(( wd2.bytes_written - wd1.bytes_written)/1024./1024. AS NUMERIC(21,1)) + ELSE 0 + END AS [MB Read/Written], + wd2.avg_stall_write_ms AS [Avg Stall (ms)], + wd1.PhysicalName AS [file physical name] + FROM #FileStats wd2 + JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime + AND wd1.DatabaseID = wd2.DatabaseID + AND wd1.FileID = wd2.FileID + ) + SELECT + Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] + FROM readstats + WHERE StallRank <=20 AND [MB Read/Written] > 0 + UNION ALL + SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] + FROM writestats + WHERE StallRank <=20 AND [MB Read/Written] > 0 + ORDER BY Pattern, StallRank; -IF (@StopAt IS NOT NULL AND @OnlyLogsAfter IS NULL) - BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @BackupDateTime) AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt) - ORDER BY BackupFile; - - OPEN BackupFiles; - END; + ------------------------- + --What happened: #PerfmonStats + ------------------------- -IF (@StandbyMode = 1) - BEGIN - IF (@StandbyUndoPath IS NULL) - RAISERROR('The file path of the undo file for standby mode was not specified. Logs will not be restored in standby mode.', 0, 1) WITH NOWAIT; - ELSE - SET @LogRecoveryOption = N'STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf'''; - END; + IF @OutputResultSets LIKE N'%PerfmonStats%' + SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, + pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, + pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, + pLast.cntr_value - pFirst.cntr_value AS ValueDelta, + ((1.0 * pLast.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pLast.SampleTime)) AS ValuePerSecond + FROM #PerfmonStats pLast + INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) + AND pLast.ID > pFirst.ID + WHERE pLast.cntr_value <> pFirst.cntr_value + ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name; -IF (@LogRecoveryOption = N'') - BEGIN - SET @LogRecoveryOption = N'NORECOVERY'; - END; --- Loop through all the files for the database -FETCH NEXT FROM BackupFiles INTO @BackupFile; - WHILE @@FETCH_STATUS = 0 - BEGIN - IF @i = 1 - + ------------------------- + --What happened: #QueryStats + ------------------------- + IF @CheckProcedureCache = 1 AND @OutputResultSets LIKE N'%BlitzCache%' BEGIN - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathLog + @BackupFile); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @HeadersSQL, @BackupPathLog, @BackupFile'; - PRINT @sql; - END; - EXECUTE (@sql); - - SELECT @LogFirstLSN = CAST(FirstLSN AS NUMERIC(25, 0)), - @LogLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) - FROM #Headers - WHERE BackupType = 2; - - IF (@ContinueLogs = 0 AND @LogFirstLSN <= @FullLastLSN AND @FullLastLSN <= @LogLastLSN AND @RestoreDiff = 0) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 0) - SET @i = 2; - - IF (@ContinueLogs = 0 AND @LogFirstLSN <= @DiffLastLSN AND @DiffLastLSN <= @LogLastLSN AND @RestoreDiff = 1) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 1) - SET @i = 2; - - DELETE FROM #Headers WHERE BackupType = 2; - - + SELECT qsNow.*, qsFirst.* + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.Pass = 2; END; - - IF @i = 1 - BEGIN - - IF @Debug = 1 RAISERROR('No Log to Restore', 0, 1) WITH NOWAIT; - - END - - IF @i = 2 + ELSE IF @OutputResultSets LIKE N'%BlitzCache%' BEGIN - - RAISERROR('@i set to 2, restoring logs', 0, 1) WITH NOWAIT; - - SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathLog + @BackupFile + N''' WITH ' + @LogRecoveryOption + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE LOG: @RestoreDatabaseName, @BackupPathLog, @BackupFile'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE LOG', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; + SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details]; END; - - FETCH NEXT FROM BackupFiles INTO @BackupFile; - END; - - CLOSE BackupFiles; - -DEALLOCATE BackupFiles; - - IF @Debug = 1 - BEGIN - SELECT '#Headers' AS table_name, * FROM #Headers AS h - END - - -END - --- Put database in a useable state -IF @RunRecovery = 1 - BEGIN - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' WITH RECOVERY' + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @RestoreDatabaseName'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; - END; - --- Ensure simple recovery model -IF @ForceSimpleRecovery = 1 - BEGIN - SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + N' SET RECOVERY SIMPLE' + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for SET RECOVERY SIMPLE: @RestoreDatabaseName'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; - END; - - -- Run checkdb against this database -IF @RunCheckDB = 1 - BEGIN - SET @sql = N'EXECUTE [dbo].[DatabaseIntegrityCheck] @Databases = ' + @RestoreDatabaseName + N', @LogToTable = ''Y''' + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for Run Integrity Check: @RestoreDatabaseName'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sys.sp_executesql @sql; - END; - - -- If test restore then blow the database away (be careful) -IF @TestRestore = 1 - BEGIN - SET @sql = N'DROP DATABASE ' + @RestoreDatabaseName + NCHAR(13); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE: @RestoreDatabaseName'; - PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; - - END; - -GO - -USE [master]; -GO - -IF OBJECT_ID('dbo.sp_foreachdb') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_foreachdb AS RETURN 0'); -GO - -ALTER PROCEDURE dbo.sp_foreachdb - -- Original fields from sp_MSforeachdb... - @command1 NVARCHAR(MAX) = NULL, - @replacechar NCHAR(1) = N'?' , - @command2 NVARCHAR(MAX) = NULL , - @command3 NVARCHAR(MAX) = NULL , - @precommand NVARCHAR(MAX) = NULL , - @postcommand NVARCHAR(MAX) = NULL , - -- Additional fields for our sp_foreachdb! - @command NVARCHAR(MAX) = NULL, --For backwards compatibility - @print_dbname BIT = 0 , - @print_command_only BIT = 0 , - @suppress_quotename BIT = 0 , - @system_only BIT = NULL , - @user_only BIT = NULL , - @name_pattern NVARCHAR(300) = N'%' , - @database_list NVARCHAR(MAX) = NULL , - @exclude_list NVARCHAR(MAX) = NULL , - @recovery_model_desc NVARCHAR(120) = NULL , - @compatibility_level TINYINT = NULL , - @state_desc NVARCHAR(120) = N'ONLINE' , - @is_read_only BIT = 0 , - @is_auto_close_on BIT = NULL , - @is_auto_shrink_on BIT = NULL , - @is_broker_enabled BIT = NULL , - @VersionDate DATETIME = NULL OUTPUT -AS - BEGIN - SET NOCOUNT ON; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - - IF ( (@command1 IS NOT NULL AND @command IS NOT NULL) - OR (@command1 IS NULL AND @command IS NULL) ) - BEGIN - RAISERROR('You must supply either @command1 or @command, but not both.',16,1); - RETURN -1; END; - SET @command1 = COALESCE(@command1,@command); - - DECLARE @sql NVARCHAR(MAX) , - @dblist NVARCHAR(MAX) , - @exlist NVARCHAR(MAX) , - @db NVARCHAR(300) , - @i INT; - - IF @database_list > N'' - BEGIN - ; - WITH n ( n ) - AS ( SELECT ROW_NUMBER() OVER ( ORDER BY s1.name ) - - 1 - FROM sys.objects AS s1 - CROSS JOIN sys.objects AS s2 - ) - SELECT @dblist = REPLACE(REPLACE(REPLACE(x, '', - ','), '', ''), - '', '') - FROM ( SELECT DISTINCT - x = 'N''' - + LTRIM(RTRIM(SUBSTRING(@database_list, - n, - CHARINDEX(',', - @database_list - + ',', n) - n))) - + '''' - FROM n - WHERE n <= LEN(@database_list) - AND SUBSTRING(',' + @database_list, n, - 1) = ',' - FOR - XML PATH('') - ) AS y ( x ); - END --- Added for @exclude_list - IF @exclude_list > N'' - BEGIN - ; - WITH n ( n ) - AS ( SELECT ROW_NUMBER() OVER ( ORDER BY s1.name ) - - 1 - FROM sys.objects AS s1 - CROSS JOIN sys.objects AS s2 - ) - SELECT @exlist = REPLACE(REPLACE(REPLACE(x, '', - ','), '', ''), - '', '') - FROM ( SELECT DISTINCT - x = 'N''' - + LTRIM(RTRIM(SUBSTRING(@exclude_list, - n, - CHARINDEX(',', - @exclude_list - + ',', n) - n))) - + '''' - FROM n - WHERE n <= LEN(@exclude_list) - AND SUBSTRING(',' + @exclude_list, n, - 1) = ',' - FOR - XML PATH('') - ) AS y ( x ); - END - - CREATE TABLE #x ( db NVARCHAR(300) ); - - SET @sql = N'SELECT name FROM sys.databases d WHERE 1=1' - + CASE WHEN @system_only = 1 THEN ' AND d.database_id IN (1,2,3,4)' - ELSE '' - END - + CASE WHEN @user_only = 1 - THEN ' AND d.database_id NOT IN (1,2,3,4)' - ELSE '' - END --- To exclude databases from changes - + CASE WHEN @exlist IS NOT NULL - THEN ' AND d.name NOT IN (' + @exlist + ')' - ELSE '' - END + CASE WHEN @name_pattern <> N'%' - THEN ' AND d.name LIKE N''' + REPLACE(@name_pattern, - '''', '''''') - + '''' - ELSE '' - END + CASE WHEN @dblist IS NOT NULL - THEN ' AND d.name IN (' + @dblist + ')' - ELSE '' - END - + CASE WHEN @recovery_model_desc IS NOT NULL - THEN ' AND d.recovery_model_desc = N''' - + @recovery_model_desc + '''' - ELSE '' - END - + CASE WHEN @compatibility_level IS NOT NULL - THEN ' AND d.compatibility_level = ' - + RTRIM(@compatibility_level) - ELSE '' - END - + CASE WHEN @state_desc IS NOT NULL - THEN ' AND d.state_desc = N''' + @state_desc + '''' - ELSE '' - END - + CASE WHEN @state_desc = 'ONLINE' AND SERVERPROPERTY('IsHadrEnabled') = 1 - THEN ' AND NOT EXISTS (SELECT 1 - FROM sys.dm_hadr_database_replica_states drs - JOIN sys.availability_replicas ar - ON ar.replica_id = drs.replica_id - JOIN sys.dm_hadr_availability_group_states ags - ON ags.group_id = ar.group_id - WHERE drs.database_id = d.database_id - AND ar.secondary_role_allow_connections = 0 - AND ags.primary_replica <> @@SERVERNAME)' - ELSE '' - END - + CASE WHEN @is_read_only IS NOT NULL - THEN ' AND d.is_read_only = ' + RTRIM(@is_read_only) - ELSE '' - END - + CASE WHEN @is_auto_close_on IS NOT NULL - THEN ' AND d.is_auto_close_on = ' + RTRIM(@is_auto_close_on) - ELSE '' - END - + CASE WHEN @is_auto_shrink_on IS NOT NULL - THEN ' AND d.is_auto_shrink_on = ' + RTRIM(@is_auto_shrink_on) - ELSE '' - END - + CASE WHEN @is_broker_enabled IS NOT NULL - THEN ' AND d.is_broker_enabled = ' + RTRIM(@is_broker_enabled) - ELSE '' - END; - - INSERT #x - EXEC sp_executesql @sql; - - DECLARE c CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY - FOR - SELECT CASE WHEN @suppress_quotename = 1 THEN db - ELSE QUOTENAME(db) - END - FROM #x - ORDER BY db; + DROP TABLE #BlitzFirstResults; - OPEN c; + /* What's running right now? This is the first and last result set. */ + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_End%' + BEGIN + IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL + BEGIN + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; + ELSE + BEGIN + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ - FETCH NEXT FROM c INTO @db; +END; /* IF @LogMessage IS NULL */ +END; /* ELSE IF @OutputType = 'SCHEMA' */ - WHILE @@FETCH_STATUS = 0 - BEGIN - SET @sql = REPLACE(@command1, @replacechar, @db); +SET NOCOUNT OFF; +GO - IF @suppress_quotename = 0 SET @sql = REPLACE(REPLACE(@sql,'[[','['),']]',']'); - IF @print_command_only = 1 - BEGIN - PRINT '/* For ' + @db + ': */' + CHAR(13) + CHAR(10) - + CHAR(13) + CHAR(10) + @sql + CHAR(13) + CHAR(10) - + CHAR(13) + CHAR(10); - END - ELSE - BEGIN - IF @print_dbname = 1 - BEGIN - PRINT '/* ' + @db + ' */'; - END - EXEC sp_executesql @sql; - END +/* How to run it: +EXEC dbo.sp_BlitzFirst - FETCH NEXT FROM c INTO @db; - END +With extra diagnostic info: +EXEC dbo.sp_BlitzFirst @ExpertMode = 1; - CLOSE c; - DEALLOCATE c; - END -GO +Saving output to tables: +EXEC sp_BlitzFirst + @OutputDatabaseName = 'DBAtools' +, @OutputSchemaName = 'dbo' +, @OutputTableName = 'BlitzFirst' +, @OutputTableNameFileStats = 'BlitzFirst_FileStats' +, @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' +, @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' +, @OutputTableNameBlitzCache = 'BlitzCache' +, @OutputTableNameBlitzWho = 'BlitzWho' +, @OutputType = 'none' +*/ diff --git a/Install-Azure.sql b/Install-Azure.sql new file mode 100644 index 000000000..5f0a15cdf --- /dev/null +++ b/Install-Azure.sql @@ -0,0 +1,26521 @@ +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON + +IF NOT EXISTS (SELECT * FROM sys.objects WHERE [object_id] = OBJECT_ID(N'[dbo].[sp_BlitzAnalysis]') AND [type] in (N'P', N'PC')) +BEGIN +EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[sp_BlitzAnalysis] AS' +END +GO + +ALTER PROCEDURE [dbo].[sp_BlitzAnalysis] ( +@Help TINYINT = 0, +@StartDate DATETIMEOFFSET(7) = NULL, +@EndDate DATETIMEOFFSET(7) = NULL, +@OutputDatabaseName NVARCHAR(256) = 'DBAtools', +@OutputSchemaName NVARCHAR(256) = N'dbo', +@OutputTableNameBlitzFirst NVARCHAR(256) = N'BlitzFirst', +@OutputTableNameFileStats NVARCHAR(256) = N'BlitzFirst_FileStats', +@OutputTableNamePerfmonStats NVARCHAR(256) = N'BlitzFirst_PerfmonStats', +@OutputTableNameWaitStats NVARCHAR(256) = N'BlitzFirst_WaitStats', +@OutputTableNameBlitzCache NVARCHAR(256) = N'BlitzCache', +@OutputTableNameBlitzWho NVARCHAR(256) = N'BlitzWho', +@Servername NVARCHAR(128) = @@SERVERNAME, +@Databasename NVARCHAR(128) = NULL, +@BlitzCacheSortorder NVARCHAR(20) = N'cpu', +@MaxBlitzFirstPriority INT = 249, +@ReadLatencyThreshold INT = 100, +@WriteLatencyThreshold INT = 100, +@WaitStatsTop TINYINT = 10, +@Version VARCHAR(30) = NULL OUTPUT, +@VersionDate DATETIME = NULL OUTPUT, +@VersionCheckMode BIT = 0, +@BringThePain BIT = 0, +@Maxdop INT = 1, +@Debug BIT = 0 +) +AS +SET NOCOUNT ON; +SET STATISTICS XML OFF; + +SELECT @Version = '8.26', @VersionDate = '20251002'; + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +IF (@Help = 1) +BEGIN + PRINT 'EXEC sp_BlitzAnalysis +@StartDate = NULL, /* Specify a datetime or NULL will get an hour ago */ +@EndDate = NULL, /* Specify a datetime or NULL will get an hour of data since @StartDate */ +@OutputDatabaseName = N''DBA'', /* Specify the database name where where we can find your logged blitz data */ +@OutputSchemaName = N''dbo'', /* Specify the schema */ +@OutputTableNameBlitzFirst = N''BlitzFirst'', /* Table name where you are storing sp_BlitzFirst output, Set to NULL to ignore */ +@OutputTableNameFileStats = N''BlitzFirst_FileStats'', /* Table name where you are storing sp_BlitzFirst filestats output, Set to NULL to ignore */ +@OutputTableNamePerfmonStats = N''BlitzFirst_PerfmonStats'', /* Table name where you are storing sp_BlitzFirst Perfmon output, Set to NULL to ignore */ +@OutputTableNameWaitStats = N''BlitzFirst_WaitStats'', /* Table name where you are storing sp_BlitzFirst Wait stats output, Set to NULL to ignore */ +@OutputTableNameBlitzCache = N''BlitzCache'', /* Table name where you are storing sp_BlitzCache output, Set to NULL to ignore */ +@OutputTableNameBlitzWho = N''BlitzWho'', /* Table name where you are storing sp_BlitzWho output, Set to NULL to ignore */ +@Databasename = NULL, /* Filters results for BlitzCache, FileStats (will also include tempdb), BlitzWho. Leave as NULL for all databases */ +@MaxBlitzFirstPriority = 249, /* Max priority to include in the results */ +@BlitzCacheSortorder = ''cpu'', /* Accepted values ''all'' ''cpu'' ''reads'' ''writes'' ''duration'' ''executions'' ''memory grant'' ''spills'' */ +@WaitStatsTop = 3, /* Controls the top for wait stats only */ +@Maxdop = 1, /* Control the degree of parallelism that the queries within this proc can use if they want to*/ +@Debug = 0; /* Show sp_BlitzAnalysis SQL commands in the messages tab as they execute */ + +/* +Additional parameters: +@ReadLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_read_average_ms in your filestats table */ +@WriteLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_write_average_ms in your filestats table */ +@BringThePain BIT /* Default: 0 - If you are getting more than 4 hours of data with blitzcachesortorder set to ''all'' you will need to set BringThePain to 1 */ +*/'; + RETURN; +END + +/* Declare all local variables required */ +DECLARE @FullOutputTableNameBlitzFirst NVARCHAR(1000); +DECLARE @FullOutputTableNameFileStats NVARCHAR(1000); +DECLARE @FullOutputTableNamePerfmonStats NVARCHAR(1000); +DECLARE @FullOutputTableNameWaitStats NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzCache NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzWho NVARCHAR(1000); +DECLARE @Sql NVARCHAR(MAX); +DECLARE @NewLine NVARCHAR(2) = CHAR(13); +DECLARE @IncludeMemoryGrants BIT; +DECLARE @IncludeSpills BIT; + +/* Validate the database name */ +IF (DB_ID(@OutputDatabaseName) IS NULL) +BEGIN + RAISERROR('Invalid database name provided for parameter @OutputDatabaseName: %s',11,0,@OutputDatabaseName); + RETURN; +END + +/* Set fully qualified table names */ +SET @FullOutputTableNameBlitzFirst = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzFirst); +SET @FullOutputTableNameFileStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameFileStats+N'_Deltas'); +SET @FullOutputTableNamePerfmonStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNamePerfmonStats+N'_Actuals'); +SET @FullOutputTableNameWaitStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameWaitStats+N'_Deltas'); +SET @FullOutputTableNameBlitzCache = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzCache); +SET @FullOutputTableNameBlitzWho = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzWho+N'_Deltas'); + +IF OBJECT_ID('tempdb.dbo.#BlitzFirstCounts') IS NOT NULL +BEGIN + DROP TABLE #BlitzFirstCounts; +END + +CREATE TABLE #BlitzFirstCounts ( + [Priority] TINYINT NOT NULL, + [FindingsGroup] VARCHAR(50) NOT NULL, + [Finding] VARCHAR(200) NOT NULL, + [TotalOccurrences] INT NULL, + [FirstOccurrence] DATETIMEOFFSET(7) NULL, + [LastOccurrence] DATETIMEOFFSET(7) NULL +); + +/* Validate variables and set defaults as required */ +IF (@BlitzCacheSortorder IS NULL) +BEGIN + SET @BlitzCacheSortorder = N'cpu'; +END + +SET @BlitzCacheSortorder = LOWER(@BlitzCacheSortorder); + +IF (@OutputTableNameBlitzCache IS NOT NULL AND @BlitzCacheSortorder NOT IN (N'all',N'cpu',N'reads',N'writes',N'duration',N'executions',N'memory grant',N'spills')) +BEGIN + RAISERROR('Invalid sort option specified for @BlitzCacheSortorder, supported values are ''all'', ''cpu'', ''reads'', ''writes'', ''duration'', ''executions'', ''memory grant'', ''spills''',11,0) WITH NOWAIT; + RETURN; +END + +/* Set @Maxdop to 1 if NULL was passed in */ +IF (@Maxdop IS NULL) +BEGIN + SET @Maxdop = 1; +END + +/* iF @Maxdop is set higher than the core count just set it to 0 */ +IF (@Maxdop > (SELECT CAST(cpu_count AS INT) FROM sys.dm_os_sys_info)) +BEGIN + SET @Maxdop = 0; +END + +/* We need to check if your SQL version has memory grant and spills columns in sys.dm_exec_query_stats */ +SELECT @IncludeMemoryGrants = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb')) THEN 1 + ELSE 0 + END; + +SELECT @IncludeSpills = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills')) THEN 1 + ELSE 0 + END; + + +IF (@StartDate IS NULL) +BEGIN + RAISERROR('Setting @StartDate to: 1 hour ago',0,0) WITH NOWAIT; + /* Set StartDate to be an hour ago */ + SET @StartDate = DATEADD(HOUR,-1,SYSDATETIMEOFFSET()); + + IF (@EndDate IS NULL) + BEGIN + RAISERROR('Setting @EndDate to: Now',0,0) WITH NOWAIT; + /* Get data right up to now */ + SET @EndDate = SYSDATETIMEOFFSET(); + END +END + +IF (@EndDate IS NULL) +BEGIN + /* Default to an hour of data or SYSDATETIMEOFFSET() if now is earlier than the hour added to @StartDate */ + IF(DATEADD(HOUR,1,@StartDate) < SYSDATETIMEOFFSET()) + BEGIN + RAISERROR('@EndDate was NULL - Setting to return 1 hour of information, if you want more then set @EndDate aswell',0,0) WITH NOWAIT; + SET @EndDate = DATEADD(HOUR,1,@StartDate); + END + ELSE + BEGIN + RAISERROR('@EndDate was NULL - Setting to SYSDATETIMEOFFSET()',0,0) WITH NOWAIT; + SET @EndDate = SYSDATETIMEOFFSET(); + END +END + +/* Default to dbo schema if NULL is passed in */ +IF (@OutputSchemaName IS NULL) +BEGIN + SET @OutputSchemaName = 'dbo'; +END + +/* Prompt the user for @BringThePain = 1 if they are searching a timeframe greater than 4 hours and they are using BlitzCacheSortorder = 'all' */ +IF(@BlitzCacheSortorder = 'all' AND DATEDIFF(HOUR,@StartDate,@EndDate) > 4 AND @BringThePain = 0) +BEGIN + RAISERROR('Wow! hold up now, are you sure you wanna do this? Are sure you want to query over 4 hours of data with @BlitzCacheSortorder set to ''all''? IF you do then set @BringThePain = 1 but I gotta warn you this might hurt a bit!',11,1) WITH NOWAIT; + RETURN; +END + +/* Output report window information */ +SELECT + @Servername AS [ServerToReportOn], + CAST(1 AS NVARCHAR(20)) + N' - '+ CAST(@MaxBlitzFirstPriority AS NVARCHAR(20)) AS [PrioritesToInclude], + @StartDate AS [StartDatetime], + @EndDate AS [EndDatetime];; + + +/* BlitzFirst data */ +SET @Sql = N' +INSERT INTO #BlitzFirstCounts ([Priority],[FindingsGroup],[Finding],[TotalOccurrences],[FirstOccurrence],[LastOccurrence]) +SELECT +[Priority], +[FindingsGroup], +[Finding], +COUNT(*) AS [TotalOccurrences], +MIN(CheckDate) AS [FirstOccurrence], +MAX(CheckDate) AS [LastOccurrence] +FROM '+@FullOutputTableNameBlitzFirst+N' +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND CheckDate BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +GROUP BY [Priority],[FindingsGroup],[Finding]; + +IF EXISTS(SELECT 1 FROM #BlitzFirstCounts) +BEGIN + SELECT + [Priority], + [FindingsGroup], + [Finding], + [TotalOccurrences], + [FirstOccurrence], + [LastOccurrence] + FROM #BlitzFirstCounts + ORDER BY [Priority] ASC,[TotalOccurrences] DESC; +END +ELSE +BEGIN + SELECT N''No findings with a priority between 1 and ''+CAST(@MaxBlitzFirstPriority AS NVARCHAR(10))+N'' found for this period''; +END +SELECT + [ServerName] +,[CheckDate] +,[CheckID] +,[Priority] +,[Finding] +,[URL] +,[Details] +,[HowToStopIt] +,[QueryPlan] +,[QueryText] +FROM '+@FullOutputTableNameBlitzFirst+N' Findings +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND [CheckDate] BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +ORDER BY CheckDate ASC,[Priority] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + + +RAISERROR('Getting BlitzFirst info from %s',0,0,@FullOutputTableNameBlitzFirst) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzFirst) IS NULL) +BEGIN + IF (@OutputTableNameBlitzFirst IS NULL) + BEGIN + RAISERROR('BlitzFirst data skipped',10,0); + SELECT N'Skipped logged BlitzFirst data as NULL was passed to parameter @OutputTableNameBlitzFirst'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzFirst data: %s does not exist',10,0,@FullOutputTableNameBlitzFirst); + SELECT N'No BlitzFirst data available as the table cannot be found'; + END + +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @MaxBlitzFirstPriority INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @MaxBlitzFirstPriority = @MaxBlitzFirstPriority; +END + +/* Blitz WaitStats data */ +SET @Sql = N'SELECT +[ServerName], +[CheckDate], +[wait_type], +[WaitsRank], +[WaitCategory], +[Ignorable], +[ElapsedSeconds], +[wait_time_ms_delta], +[wait_time_minutes_delta], +[wait_time_minutes_per_minute], +[signal_wait_time_ms_delta], +[waiting_tasks_count_delta], +ISNULL((CAST([wait_time_ms_delta] AS DECIMAL(38,2))/NULLIF(CAST([waiting_tasks_count_delta] AS DECIMAL(38,2)),0)),0) AS [wait_time_ms_per_wait] +FROM +( + SELECT + [ServerName], + [CheckDate], + [wait_type], + [WaitCategory], + [Ignorable], + [ElapsedSeconds], + [wait_time_ms_delta], + [wait_time_minutes_delta], + [wait_time_minutes_per_minute], + [signal_wait_time_ms_delta], + [waiting_tasks_count_delta], + ROW_NUMBER() OVER(PARTITION BY [CheckDate] ORDER BY [CheckDate] ASC,[wait_time_ms_delta] DESC) AS [WaitsRank] + FROM '+@FullOutputTableNameWaitStats+N' AS [Waits] + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate +) TopWaits +WHERE [WaitsRank] <= @WaitStatsTop +ORDER BY +[CheckDate] ASC, +[wait_time_ms_delta] DESC +OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting wait stats info from %s',0,0,@FullOutputTableNameWaitStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameWaitStats) IS NULL) +BEGIN + IF (@OutputTableNameWaitStats IS NULL) + BEGIN + RAISERROR('Wait stats data skipped',10,0); + SELECT N'Skipped logged wait stats data as NULL was passed to parameter @OutputTableNameWaitStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for wait stats data: %s does not exist',10,0,@FullOutputTableNameWaitStats); + SELECT N'No wait stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @WaitStatsTop TINYINT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @WaitStatsTop=@WaitStatsTop; +END + +/* BlitzFileStats info */ +SET @Sql = N' +SELECT +[ServerName], +[CheckDate], +CASE + WHEN MAX([io_stall_read_ms_average]) > @ReadLatencyThreshold THEN ''Yes'' + WHEN MAX([io_stall_write_ms_average]) > @WriteLatencyThreshold THEN ''Yes'' + ELSE ''No'' +END AS [io_stall_ms_breached], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) AS [PhysicalPath], +SUM([SizeOnDiskMB]) AS [SizeOnDiskMB], +SUM([SizeOnDiskMBgrowth]) AS [SizeOnDiskMBgrowth], +MAX([io_stall_read_ms]) AS [max_io_stall_read_ms], +MAX([io_stall_read_ms_average]) AS [max_io_stall_read_ms_average], +@ReadLatencyThreshold AS [is_stall_read_ms_threshold], +SUM([num_of_reads]) AS [num_of_reads], +SUM([megabytes_read]) AS [megabytes_read], +MAX([io_stall_write_ms]) AS [max_io_stall_write_ms], +MAX([io_stall_write_ms_average]) AS [max_io_stall_write_ms_average], +@WriteLatencyThreshold AS [io_stall_write_ms_average], +SUM([num_of_writes]) AS [num_of_writes], +SUM([megabytes_written]) AS [megabytes_written] +FROM '+@FullOutputTableNameFileStats+N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate +' ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] IN (N''tempdb'',@Databasename) +' + ELSE N'' +END ++N'GROUP BY +[ServerName], +[CheckDate], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) +ORDER BY +[CheckDate] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting FileStats info from %s',0,0,@FullOutputTableNameFileStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameFileStats) IS NULL) +BEGIN + IF (@OutputTableNameFileStats IS NULL) + BEGIN + RAISERROR('File stats data skipped',10,0); + SELECT N'Skipped logged File stats data as NULL was passed to parameter @OutputTableNameFileStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for FileStats data: %s does not exist',10,0,@FullOutputTableNameFileStats); + SELECT N'No File stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @ReadLatencyThreshold INT, + @WriteLatencyThreshold INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename, + @ReadLatencyThreshold = @ReadLatencyThreshold, + @WriteLatencyThreshold = @WriteLatencyThreshold; +END + +/* Blitz Perfmon stats*/ +SET @Sql = N' +SELECT + [ServerName] + ,[CheckDate] + ,[counter_name] + ,[object_name] + ,[instance_name] + ,[cntr_value] +FROM '+@FullOutputTableNamePerfmonStats+N' +WHERE [ServerName] = @Servername +AND CheckDate BETWEEN @StartDate AND @EndDate +ORDER BY + [CheckDate] ASC, + [counter_name] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting Perfmon info from %s',0,0,@FullOutputTableNamePerfmonStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNamePerfmonStats) IS NULL) +BEGIN + IF (@OutputTableNamePerfmonStats IS NULL) + BEGIN + RAISERROR('Perfmon stats data skipped',10,0); + SELECT N'Skipped logged Perfmon stats data as NULL was passed to parameter @OutputTableNamePerfmonStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for Perfmon stats data: %s does not exist',10,0,@FullOutputTableNamePerfmonStats); + SELECT N'No Perfmon data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername; +END + +/* Blitz cache data */ +RAISERROR('Sortorder for BlitzCache data: %s',0,0,@BlitzCacheSortorder) WITH NOWAIT; + +/* Set intial CTE */ +SET @Sql = N'WITH CheckDates AS ( +SELECT DISTINCT CheckDate +FROM ' ++@FullOutputTableNameBlitzCache ++N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate' ++@NewLine ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] = @Databasename'+@NewLine + ELSE N'' +END ++N')' +; + +SET @Sql += @NewLine; + +/* Append additional CTEs based on sortorder */ +SET @Sql += ( +SELECT CAST(N',' AS NVARCHAR(MAX)) ++[SortOptions].[Aliasname]+N' AS ( +SELECT + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,[Sortorder] + ,[TimeFrameRank] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [OverallRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] +FROM CheckDates +CROSS APPLY ( + SELECT TOP (5) + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,'+QUOTENAME(UPPER([SortOptions].[Sortorder]),N'''')+N' AS [Sortorder] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [TimeFrameRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] + FROM '+@FullOutputTableNameBlitzCache+N' AS '+[SortOptions].[Aliasname]+N' + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate + AND ['+[SortOptions].[Aliasname]+N'].[CheckDate] = [CheckDates].[CheckDate]' + +@NewLine + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND ['+[SortOptions].[Aliasname]+N'].[DatabaseName] = @Databasename'+@NewLine + ELSE N'' + END + +CASE + WHEN [Sortorder] = N'cpu' THEN N'AND [TotalCPU] > 0' + WHEN [Sortorder] = N'reads' THEN N'AND [TotalReads] > 0' + WHEN [Sortorder] = N'writes' THEN N'AND [TotalWrites] > 0' + WHEN [Sortorder] = N'duration' THEN N'AND [TotalDuration] > 0' + WHEN [Sortorder] = N'executions' THEN N'AND [ExecutionCount] > 0' + WHEN [Sortorder] = N'memory grant' THEN N'AND [MaxGrantKB] > 0' + WHEN [Sortorder] = N'spills' THEN N'AND [MaxSpills] > 0' + ELSE N'' + END + +N' + ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) '+[SortOptions].[Aliasname]+N' +)' +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'); + +SET @Sql += @NewLine; + +/* Build the select statements to return the data after CTE declarations */ +SET @Sql += ( +SELECT STUFF(( +SELECT @NewLine ++N'UNION ALL' ++@NewLine ++N'SELECT * +FROM '+[SortOptions].[Aliasname] +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'),1,11,N'') +); + +/* Append Order By */ +SET @Sql += @NewLine ++N'ORDER BY + [Sortorder] ASC, + [CheckDate] ASC, + [TimeFrameRank] ASC'; + +/* Append OPTION(RECOMPILE, MAXDOP) to complete the statement */ +SET @Sql += @NewLine ++N'OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + +RAISERROR('Getting BlitzCache info from %s',0,0,@FullOutputTableNameBlitzCache) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT SUBSTRING(@Sql, 0, 4000); + PRINT SUBSTRING(@Sql, 4000, 8000); + PRINT SUBSTRING(@Sql, 8000, 12000); + PRINT SUBSTRING(@Sql, 12000, 16000); + PRINT SUBSTRING(@Sql, 16000, 20000); + PRINT SUBSTRING(@Sql, 20000, 24000); + PRINT SUBSTRING(@Sql, 24000, 28000); +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzCache) IS NULL) +BEGIN + IF (@OutputTableNameBlitzCache IS NULL) + BEGIN + RAISERROR('BlitzCache data skipped',10,0); + SELECT N'Skipped logged BlitzCache data as NULL was passed to parameter @OutputTableNameBlitzCache'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzCache data: %s does not exist',10,0,@FullOutputTableNameBlitzCache); + SELECT N'No BlitzCache data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @BlitzCacheSortorder NVARCHAR(20), + @StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7)', + @Servername = @Servername, + @Databasename = @Databasename, + @BlitzCacheSortorder = @BlitzCacheSortorder, + @StartDate = @StartDate, + @EndDate = @EndDate; +END + + + +/* BlitzWho data */ +SET @Sql = N' +SELECT [ServerName] + ,[CheckDate] + ,[elapsed_time] + ,[session_id] + ,[database_name] + ,[query_text_snippet] + ,[query_plan] + ,[live_query_plan] + ,[query_cost] + ,[status] + ,[wait_info] + ,[top_session_waits] + ,[blocking_session_id] + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[degree_of_parallelism] + ,[request_logical_reads] + ,[Logical_Reads_MB] + ,[request_writes] + ,[Logical_Writes_MB] + ,[request_physical_reads] + ,[Physical_reads_MB] + ,[session_cpu] + ,[session_logical_reads] + ,[session_logical_reads_MB] + ,[session_physical_reads] + ,[session_physical_reads_MB] + ,[session_writes] + ,[session_writes_MB] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads] + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info] + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset] + FROM '+@FullOutputTableNameBlitzWho+N' + WHERE [ServerName] = @Servername + AND ([CheckDate] BETWEEN @StartDate AND @EndDate OR [start_time] BETWEEN CAST(@StartDate AS DATETIME) AND CAST(@EndDate AS DATETIME)) + ' + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND [database_name] = @Databasename + ' + ELSE N'' + END ++N'ORDER BY [CheckDate] ASC + OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + +RAISERROR('Getting BlitzWho info from %s',0,0,@FullOutputTableNameBlitzWho) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzWho) IS NULL) +BEGIN + IF (@OutputTableNameBlitzWho IS NULL) + BEGIN + RAISERROR('BlitzWho data skipped',10,0); + SELECT N'Skipped logged BlitzWho data as NULL was passed to parameter @OutputTableNameBlitzWho'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzWho data: %s does not exist',10,0,@FullOutputTableNameBlitzWho); + SELECT N'No BlitzWho data available as the table cannot be found'; + END +END +ELSE +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename; +END + + +GO +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 STATISTICS IO OFF; +SET STATISTICS TIME OFF; +GO + +IF ( +SELECT + CASE + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 + ELSE 1 + END +) = 0 +BEGIN + DECLARE @msg VARCHAR(8000); + SELECT @msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); + PRINT @msg; + RETURN; +END; + +IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); +GO + +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheProcs', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheProcs;'); +GO + +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheResults', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheResults;'); +GO + +CREATE TABLE ##BlitzCacheResults ( + SPID INT, + ID INT IDENTITY(1,1), + CheckID INT, + Priority TINYINT, + FindingsGroup VARCHAR(50), + Finding VARCHAR(500), + URL VARCHAR(200), + Details VARCHAR(4000) +); + +CREATE TABLE ##BlitzCacheProcs ( + SPID INT , + QueryType NVARCHAR(258), + DatabaseName sysname, + AverageCPU DECIMAL(38,4), + AverageCPUPerMinute DECIMAL(38,4), + TotalCPU DECIMAL(38,4), + PercentCPUByType MONEY, + PercentCPU MONEY, + AverageDuration DECIMAL(38,4), + TotalDuration DECIMAL(38,4), + PercentDuration MONEY, + PercentDurationByType MONEY, + AverageReads BIGINT, + TotalReads BIGINT, + PercentReads MONEY, + PercentReadsByType MONEY, + ExecutionCount BIGINT, + PercentExecutions MONEY, + PercentExecutionsByType MONEY, + ExecutionsPerMinute MONEY, + TotalWrites BIGINT, + AverageWrites MONEY, + PercentWrites MONEY, + PercentWritesByType MONEY, + WritesPerMinute MONEY, + PlanCreationTime DATETIME, + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' + ELSE 'N/A' END, + SqlHandle VARBINARY(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' + ELSE 'N/A' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' + ELSE 'N/A' END, + QueryHash BINARY(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' + ELSE 'N/A' END, + QueryPlanHash BINARY(8), + StatementStartOffset INT, + StatementEndOffset INT, + PlanGenerationNum BIGINT, + MinReturnedRows BIGINT, + MaxReturnedRows BIGINT, + AverageReturnedRows MONEY, + TotalReturnedRows BIGINT, + LastReturnedRows BIGINT, + /*The Memory Grant columns are only supported + in certain versions, giggle giggle. + */ + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryText NVARCHAR(MAX), + QueryPlan XML, + /* these next four columns are the total for the type of query. + don't actually use them for anything apart from math by type. + */ + TotalWorkerTimeForType BIGINT, + TotalElapsedTimeForType BIGINT, + TotalReadsForType DECIMAL(30), + TotalExecutionCountForType BIGINT, + TotalWritesForType DECIMAL(30), + NumberOfPlans INT, + NumberOfDistinctPlans INT, + SerialDesiredMemory FLOAT, + SerialRequiredMemory FLOAT, + CachedPlanSize FLOAT, + CompileTime FLOAT, + CompileCPU FLOAT , + CompileMemory FLOAT , + MaxCompileMemory FLOAT , + min_worker_time BIGINT, + max_worker_time BIGINT, + is_forced_plan BIT, + is_forced_parameterized BIT, + is_cursor BIT, + is_optimistic_cursor BIT, + is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, + is_parallel BIT, + is_forced_serial BIT, + is_key_lookup_expensive BIT, + key_lookup_cost FLOAT, + is_remote_query_expensive BIT, + remote_query_cost FLOAT, + frequent_execution BIT, + parameter_sniffing BIT, + unparameterized_query BIT, + near_parallel BIT, + plan_warnings BIT, + plan_multiple_plans INT, + long_running BIT, + downlevel_estimator BIT, + implicit_conversions BIT, + busy_loops BIT, + tvf_join BIT, + tvf_estimate BIT, + compile_timeout BIT, + compile_memory_limit_exceeded BIT, + warning_no_join_predicate BIT, + QueryPlanCost FLOAT, + missing_index_count INT, + unmatched_index_count INT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + age_minutes MONEY, + age_minutes_lifetime MONEY, + is_trivial BIT, + trace_flags_session VARCHAR(1000), + is_unused_grant BIT, + function_count INT, + clr_function_count INT, + is_table_variable BIT, + no_stats_warning BIT, + relop_warnings BIT, + is_table_scan BIT, + backwards_scan BIT, + forced_index BIT, + forced_seek BIT, + forced_scan BIT, + columnstore_row_mode BIT, + is_computed_scalar BIT , + is_sort_expensive BIT, + sort_cost FLOAT, + is_computed_filter BIT, + op_name VARCHAR(100) NULL, + index_insert_count INT NULL, + index_update_count INT NULL, + index_delete_count INT NULL, + cx_insert_count INT NULL, + cx_update_count INT NULL, + cx_delete_count INT NULL, + table_insert_count INT NULL, + table_update_count INT NULL, + table_delete_count INT NULL, + index_ops AS (index_insert_count + index_update_count + index_delete_count + + cx_insert_count + cx_update_count + cx_delete_count + + table_insert_count + table_update_count + table_delete_count), + is_row_level BIT, + is_spatial BIT, + index_dml BIT, + table_dml BIT, + long_running_low_cpu BIT, + low_cost_high_cpu BIT, + stale_stats BIT, + is_adaptive BIT, + index_spool_cost FLOAT, + index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, + is_spool_expensive BIT, + is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, + estimated_rows FLOAT, + is_bad_estimate BIT, + is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, + implicit_conversion_info XML, + cached_execution_parameters XML, + missing_indexes XML, + SetOptions VARCHAR(MAX), + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) + ); +GO + +ALTER PROCEDURE dbo.sp_BlitzCache + @Help BIT = 0, + @Top INT = NULL, + @SortOrder VARCHAR(50) = 'CPU', + @UseTriggersAnyway BIT = NULL, + @ExportToExcel BIT = 0, + @ExpertMode TINYINT = 0, + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(258) = NULL , + @OutputDatabaseName NVARCHAR(258) = NULL , + @OutputSchemaName NVARCHAR(258) = NULL , + @OutputTableName NVARCHAR(258) = NULL , -- do NOT use ##BlitzCacheResults or ##BlitzCacheProcs as they are used as work tables in this procedure + @ConfigurationDatabaseName NVARCHAR(128) = NULL , + @ConfigurationSchemaName NVARCHAR(258) = NULL , + @ConfigurationTableName NVARCHAR(258) = NULL , + @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 , + @IgnoreSqlHandles VARCHAR(MAX) = NULL , + @QueryFilter VARCHAR(10) = 'ALL' , + @DatabaseName NVARCHAR(128) = NULL , + @StoredProcName NVARCHAR(128) = NULL, + @SlowlySearchPlansFor NVARCHAR(4000) = NULL, + @Reanalyze BIT = 0 , + @SkipAnalysis BIT = 0 , + @BringThePain BIT = 0 , + @MinimumExecutionCount INT = 0, + @Debug BIT = 0, + @CheckDateOverride DATETIMEOFFSET = NULL, + @MinutesBack INT = NULL, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @KeepCRLF BIT = 0 +WITH RECOMPILE +AS +BEGIN +SET NOCOUNT ON; +SET STATISTICS XML OFF; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; + +IF @Help = 1 + BEGIN + PRINT ' + sp_BlitzCache from http://FirstResponderKit.org + + This script displays your most resource-intensive queries from the plan cache, + and points to ways you can tune these queries to make them faster. + + + 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. + + Known limitations of this version: + - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is + excluded by default. + - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes + with no spaces between the hash values. + + Unknown limitations of this version: + - May or may not be vulnerable to the wick effect. + + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + + + + MIT License + + Copyright (c) Brent Ozar Unlimited + + 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. + '; + + + SELECT N'@Help' AS [Parameter Name] , + N'BIT' AS [Data Type] , + N'Displays this help message.' AS [Parameter Description] + + UNION ALL + SELECT N'@Top', + N'INT', + N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' + + UNION ALL + SELECT N'@SortOrder', + N'VARCHAR(10)', + N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant", "Unused Grant", "Spills", "Query Hash", "Duplicate". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' + + UNION ALL + SELECT N'@UseTriggersAnyway', + N'BIT', + N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' + + UNION ALL + SELECT N'@ExportToExcel', + N'BIT', + N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' + + UNION ALL + SELECT N'@ExpertMode', + N'TINYINT', + N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' + UNION ALL + SELECT N'@OutputType', + N'NVARCHAR(258)', + N'If set to "NONE", this will tell this procedure not to run any query leading to a results set thrown to caller.' + + UNION ALL + SELECT N'@OutputDatabaseName', + N'NVARCHAR(128)', + N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' + + UNION ALL + SELECT N'@OutputSchemaName', + N'NVARCHAR(258)', + N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' + + UNION ALL + SELECT N'@OutputTableName', + N'NVARCHAR(258)', + N'The output table. If this does not exist, it will be created for you.' + + UNION ALL + SELECT N'@DurationFilter', + N'DECIMAL(38,4)', + N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' + + UNION ALL + SELECT N'@HideSummary', + N'BIT', + N'Hides the findings summary result set.' + + UNION ALL + SELECT N'@IgnoreSystemDBs', + N'BIT', + N'Ignores plans found in the system databases (master, model, msdb, tempdb, dbmaintenance, dbadmin, dbatools and resourcedb)' + + UNION ALL + SELECT N'@OnlyQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' + + UNION ALL + SELECT N'@IgnoreQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to ignore.' + + UNION ALL + SELECT N'@OnlySqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to use for filtering results.' + + UNION ALL + SELECT N'@IgnoreSqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to ignore.' + + UNION ALL + SELECT N'@DatabaseName', + N'NVARCHAR(128)', + N'A database name which is used for filtering results.' + + UNION ALL + SELECT N'@StoredProcName', + N'NVARCHAR(128)', + N'Name of stored procedure you want to find plans for.' + + UNION ALL + SELECT N'@SlowlySearchPlansFor', + N'NVARCHAR(4000)', + N'String to search for in plan text. % wildcards allowed.' + + UNION ALL + SELECT N'@BringThePain', + N'BIT', + N'When using @SortOrder = ''all'' and @Top > 10, we require you to set @BringThePain = 1 so you understand that sp_BlitzCache will take a while to run.' + + UNION ALL + SELECT N'@QueryFilter', + N'VARCHAR(10)', + N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' + + UNION ALL + SELECT N'@Reanalyze', + N'BIT', + N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' + + UNION ALL + SELECT N'@MinimumExecutionCount', + N'INT', + N'Queries with fewer than this number of executions will be omitted from results.' + + UNION ALL + SELECT N'@Debug', + N'BIT', + N'Setting this to 1 will print dynamic SQL and select data from all tables used.' + + UNION ALL + SELECT N'@MinutesBack', + N'INT', + N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negative.' + + UNION ALL + SELECT N'@Version', + N'VARCHAR(30)', + N'OUTPUT parameter holding version number.' + + UNION ALL + SELECT N'@VersionDate', + N'DATETIME', + N'OUTPUT parameter holding version date.' + + UNION ALL + SELECT N'@VersionCheckMode', + N'BIT', + 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 */ + SELECT N'# Executions' AS [Column Name], + N'BIGINT' AS [Data Type], + N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] + + UNION ALL + SELECT N'Executions / Minute', + N'MONEY', + N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' + + UNION ALL + SELECT N'Execution Weight', + N'MONEY', + N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' + + UNION ALL + SELECT N'Database', + N'sysname', + N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' + + UNION ALL + SELECT N'Total CPU', + N'BIGINT', + N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' + + UNION ALL + SELECT N'Avg CPU', + N'BIGINT', + N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' + + UNION ALL + SELECT N'CPU Weight', + N'MONEY', + N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' + + UNION ALL + SELECT N'Total Duration', + N'BIGINT', + N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' + + UNION ALL + SELECT N'Avg Duration', + N'BIGINT', + N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' + + UNION ALL + SELECT N'Duration Weight', + N'MONEY', + N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' + + UNION ALL + SELECT N'Total Reads', + N'BIGINT', + N'Total logical reads performed by this query since last compilation.' + + UNION ALL + SELECT N'Average Reads', + N'BIGINT', + N'Average logical reads performed by each execution of this query since the last compilation.' + + UNION ALL + SELECT N'Read Weight', + N'MONEY', + N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' + + UNION ALL + SELECT N'Total Writes', + N'BIGINT', + N'Total logical writes performed by this query since last compilation.' + + UNION ALL + SELECT N'Average Writes', + N'BIGINT', + N'Average logical writes performed by each execution this query since last compilation.' + + UNION ALL + SELECT N'Write Weight', + N'MONEY', + N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' + + UNION ALL + SELECT N'Query Type', + N'NVARCHAR(258)', + N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' + + UNION ALL + SELECT N'Query Text', + N'NVARCHAR(4000)', + N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' + + UNION ALL + SELECT N'% Executions (Type)', + N'MONEY', + N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' + + UNION ALL + SELECT N'% CPU (Type)', + N'MONEY', + N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' + + UNION ALL + SELECT N'% Duration (Type)', + N'MONEY', + N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' + + UNION ALL + SELECT N'% Reads (Type)', + N'MONEY', + N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' + + UNION ALL + SELECT N'% Writes (Type)', + N'MONEY', + N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' + + UNION ALL + SELECT N'Total Rows', + N'BIGINT', + N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' + + UNION ALL + SELECT N'Average Rows', + N'MONEY', + N'Average number of rows returned by each execution of the query.' + + UNION ALL + SELECT N'Min Rows', + N'BIGINT', + N'The minimum number of rows returned by any execution of this query.' + + UNION ALL + SELECT N'Max Rows', + N'BIGINT', + N'The maximum number of rows returned by any execution of this query.' + + UNION ALL + SELECT N'MinGrantKB', + N'BIGINT', + N'The minimum memory grant the query received in kb.' + + UNION ALL + SELECT N'MaxGrantKB', + N'BIGINT', + N'The maximum memory grant the query received in kb.' + + UNION ALL + SELECT N'MinUsedGrantKB', + N'BIGINT', + N'The minimum used memory grant the query received in kb.' + + UNION ALL + SELECT N'MaxUsedGrantKB', + N'BIGINT', + N'The maximum used memory grant the query received in kb.' + + UNION ALL + SELECT N'MinSpills', + N'BIGINT', + N'The minimum amount this query has spilled to tempdb in 8k pages.' + + UNION ALL + SELECT N'MaxSpills', + N'BIGINT', + N'The maximum amount this query has spilled to tempdb in 8k pages.' + + UNION ALL + SELECT N'TotalSpills', + N'BIGINT', + N'The total amount this query has spilled to tempdb in 8k pages.' + + UNION ALL + SELECT N'AvgSpills', + N'BIGINT', + N'The average amount this query has spilled to tempdb in 8k pages.' + + UNION ALL + SELECT N'PercentMemoryGrantUsed', + N'MONEY', + N'Result of dividing the maximum grant used by the minimum granted.' + + UNION ALL + SELECT N'AvgMaxMemoryGrant', + N'MONEY', + N'The average maximum memory grant for a query.' + + UNION ALL + SELECT N'# Plans', + N'INT', + N'The total number of execution plans found that match a given query.' + + UNION ALL + SELECT N'# Distinct Plans', + N'INT', + N'The number of distinct execution plans that match a given query. ' + + NCHAR(13) + NCHAR(10) + + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' + + UNION ALL + SELECT N'Created At', + N'DATETIME', + N'Time that the execution plan was last compiled.' + + UNION ALL + SELECT N'Last Execution', + N'DATETIME', + N'The last time that this query was executed.' + + UNION ALL + SELECT N'Query Plan', + N'XML', + N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' + + UNION ALL + SELECT N'Plan Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to the compiled plan this query is a part of.' + + UNION ALL + SELECT N'SQL Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' + + UNION ALL + SELECT N'Query Hash', + N'BINARY(8)', + N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' + + UNION ALL + SELECT N'Warnings', + N'VARCHAR(MAX)', + N'A list of individual warnings generated by this query.' ; + + + + /* Configuration table description */ + SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , + N'100' AS [Default Value] , + N'Executions / Minute' AS [Unit of Measure] , + N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] + + UNION ALL + SELECT N'Parameter Sniffing Variance Percent' , + N'30' , + N'Percent' , + N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' + + UNION ALL + SELECT N'Parameter Sniffing IO Threshold' , + N'100,000' , + N'Logical reads' , + N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' + + UNION ALL + SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' + + UNION ALL + SELECT N'Long Running Query Warning' AS [Configuration Parameter] , + N'300' , + N'Seconds' , + N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' + + UNION ALL + SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; + RETURN; + END; /* IF @Help = 1 */ + + + +/*Validate version*/ +IF ( +SELECT + CASE + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 + WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 + ELSE 1 + END +) = 0 +BEGIN + DECLARE @version_msg VARCHAR(8000); + SELECT @version_msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); + PRINT @version_msg; + RETURN; +END; + +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; +END; + +IF(@OutputType = 'NONE') +BEGIN + SET @HideSummary = 1; +END; + + +/* Lets get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = LOWER(@SortOrder); + +/* Set @Top based on sort */ +IF ( + @Top IS NULL + AND @SortOrder IN ( 'all', 'all sort' ) + ) + BEGIN + SET @Top = 5; + END; + +IF ( + @Top IS NULL + AND @SortOrder NOT IN ( 'all', 'all sort' ) + ) + BEGIN + SET @Top = 10; + END; + + +/* If they want to sort by query hash, populate the @OnlyQueryHashes list for them */ +IF @SortOrder LIKE 'query hash%' + BEGIN + RAISERROR('Beginning query hash sort', 0, 1) WITH NOWAIT; + + SELECT TOP(@Top) qs.query_hash, + MAX(qs.max_worker_time) AS max_worker_time, + COUNT_BIG(*) AS records + INTO #query_hash_grouped + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY ( SELECT pa.value + FROM sys.dm_exec_plan_attributes(qs.plan_handle) AS pa + WHERE pa.attribute = 'dbid' ) AS ca + GROUP BY qs.query_hash, ca.value + HAVING COUNT_BIG(*) > 1 + ORDER BY max_worker_time DESC, + records DESC; + + SELECT TOP (1) + @OnlyQueryHashes = STUFF((SELECT DISTINCT N',' + CONVERT(NVARCHAR(MAX), qhg.query_hash, 1) + FROM #query_hash_grouped AS qhg + WHERE qhg.query_hash <> 0x00 + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + OPTION(RECOMPILE); + + /* When they ran it, @SortOrder probably looked like 'query hash, cpu', so strip the first sort order out: */ + SELECT @SortOrder = LTRIM(REPLACE(REPLACE(@SortOrder,'query hash', ''), ',', '')); + + /* If they just called it with @SortOrder = 'query hash', set it to 'cpu' for backwards compatibility: */ + IF @SortOrder = '' SET @SortOrder = 'cpu'; + + END + + +/* If they want to sort by duplicate, create a table with the worst offenders - issue #3345 */ +IF @SortOrder LIKE 'duplicate%' + BEGIN + RAISERROR('Beginning duplicate query hash sort', 0, 1) WITH NOWAIT; + + /* Find the query hashes that are the most duplicated */ + WITH MostCommonQueries AS ( + SELECT TOP(@Top) qs.query_hash, + COUNT_BIG(*) AS plans + FROM sys.dm_exec_query_stats AS qs + GROUP BY qs.query_hash + HAVING COUNT_BIG(*) > 100 + ORDER BY COUNT_BIG(*) DESC + ) + SELECT mcq_recent.sql_handle, mcq_recent.plan_handle, mcq_recent.creation_time AS duplicate_creation_time, mcq.plans + INTO #duplicate_query_filter + FROM MostCommonQueries mcq + CROSS APPLY ( SELECT TOP 1 qs.sql_handle, qs.plan_handle, qs.creation_time + FROM sys.dm_exec_query_stats qs + WHERE qs.query_hash = mcq.query_hash + ORDER BY qs.creation_time DESC) AS mcq_recent + OPTION (RECOMPILE); + + SET @MinimumExecutionCount = 0; + END + + +/* validate user inputs */ +IF @Top IS NULL + OR @SortOrder IS NULL + OR @QueryFilter IS NULL + OR @Reanalyze IS NULL +BEGIN + RAISERROR(N'Several parameters (@Top, @SortOrder, @QueryFilter, @renalyze) are required. Do not set them to NULL. Please try again.', 16, 1) WITH NOWAIT; + RETURN; +END; + +RAISERROR(N'Checking @MinutesBack validity.', 0, 1) WITH NOWAIT; +IF @MinutesBack IS NOT NULL + BEGIN + IF @MinutesBack > 0 + BEGIN + RAISERROR(N'Setting @MinutesBack to a negative number', 0, 1) WITH NOWAIT; + SET @MinutesBack *=-1; + END; + IF @MinutesBack = 0 + BEGIN + RAISERROR(N'@MinutesBack can''t be 0, setting to -1', 0, 1) WITH NOWAIT; + SET @MinutesBack = -1; + END; + END; + + + +DECLARE @DurationFilter_i INT, + @MinMemoryPerQuery INT, + @msg NVARCHAR(4000), + @NoobSaibot BIT = 0, + @VersionShowsAirQuoteActualPlans BIT, + @ObjectFullName NVARCHAR(2000), + @user_perm_sql NVARCHAR(MAX) = N'', + @user_perm_gb_out DECIMAL(10,2), + @common_version DECIMAL(10,2), + @buffer_pool_memory_gb DECIMAL(10,2), + @user_perm_percent DECIMAL(10,2), + @is_tokenstore_big BIT = 0, + @sort NVARCHAR(MAX) = N'', + @sort_filter NVARCHAR(MAX) = N''; + + +IF @SortOrder = 'sp_BlitzIndex' +BEGIN + RAISERROR(N'OUTSTANDING!', 0, 1) WITH NOWAIT; + SET @SortOrder = 'reads'; + SET @NoobSaibot = 1; + +END + + +/* Change duration from seconds to milliseconds */ +IF @DurationFilter IS NOT NULL + BEGIN + RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; + SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); + END; + +RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; +SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; + +IF SERVERPROPERTY('EngineEdition') IN (5, 6) AND DB_NAME() <> @DatabaseName +BEGIN + RAISERROR('You specified a database name other than the current database, but Azure SQL DB does not allow you to change databases. Execute sp_BlitzCache from the database you want to analyze.', 16, 1); + RETURN; +END; +IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> N'' +BEGIN + RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); + RETURN; +END; +IF (SELECT DATABASEPROPERTYEX(ISNULL(@DatabaseName, 'master'), 'Collation')) IS NULL AND SERVERPROPERTY('EngineEdition') NOT IN (5, 6, 8) +BEGIN + RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); + RETURN; +END; + +SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; + +SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); + +SET @SortOrder = CASE + WHEN @SortOrder IN ('executions per minute','execution per minute','executions / minute','execution / minute','xpm') THEN 'avg executions' + WHEN @SortOrder IN ('recent compilations','recent compilation','compile') THEN 'compiles' + WHEN @SortOrder IN ('read') THEN 'reads' + WHEN @SortOrder IN ('avg read') THEN 'avg reads' + WHEN @SortOrder IN ('write') THEN 'writes' + WHEN @SortOrder IN ('avg write') THEN 'avg writes' + WHEN @SortOrder IN ('memory grants') THEN 'memory grant' + WHEN @SortOrder IN ('avg memory grants') THEN 'avg memory grant' + WHEN @SortOrder IN ('unused grants','unused memory', 'unused memory grant', 'unused memory grants') THEN 'unused grant' + WHEN @SortOrder IN ('spill') THEN 'spills' + WHEN @SortOrder IN ('avg spill') THEN 'avg spills' + WHEN @SortOrder IN ('execution') THEN 'executions' + WHEN @SortOrder IN ('duplicates') THEN 'duplicate' + ELSE @SortOrder END + +RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; +IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', + 'duration', 'avg duration', 'executions', 'avg executions', + 'compiles', 'memory grant', 'avg memory grant', 'unused grant', + 'spills', 'avg spills', 'all', 'all avg', 'sp_BlitzIndex', + 'query hash', 'duplicate') + BEGIN + RAISERROR(N'Invalid sort order chosen, reverting to cpu', 16, 1) WITH NOWAIT; + SET @SortOrder = 'cpu'; + END; + +SET @QueryFilter = LOWER(@QueryFilter); + +IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') + BEGIN + RAISERROR(N'Invalid query filter chosen. Reverting to all.', 0, 1) WITH NOWAIT; + SET @QueryFilter = 'all'; + END; + +IF @SkipAnalysis = 1 + BEGIN + RAISERROR(N'Skip Analysis set to 1, hiding Summary', 0, 1) WITH NOWAIT; + SET @HideSummary = 1; + END; + +DECLARE @AllSortSql NVARCHAR(MAX) = N''; +DECLARE @VersionShowsMemoryGrants BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb') + SET @VersionShowsMemoryGrants = 1; +ELSE + SET @VersionShowsMemoryGrants = 0; + +DECLARE @VersionShowsSpills BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills') + SET @VersionShowsSpills = 1; +ELSE + SET @VersionShowsSpills = 0; + +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_plan_stats') AND name = 'query_plan') + SET @VersionShowsAirQuoteActualPlans = 1; +ELSE + SET @VersionShowsAirQuoteActualPlans = 0; + +IF @Reanalyze = 1 + BEGIN + IF OBJECT_ID('tempdb..##BlitzCacheResults') IS NULL + BEGIN + RAISERROR(N'##BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; + SET @Reanalyze = 0; + END + ELSE + BEGIN + RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; + GOTO Results; + END; + END; + + +IF @SortOrder IN ('all', 'all avg') + BEGIN + RAISERROR(N'Checking all sort orders, please be patient', 0, 1) WITH NOWAIT; + GOTO AllSorts; + END; + +RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL + DROP TABLE #only_query_hashes ; + +IF OBJECT_ID('tempdb..#ignore_query_hashes') IS NOT NULL + DROP TABLE #ignore_query_hashes ; + +IF OBJECT_ID('tempdb..#only_sql_handles') IS NOT NULL + DROP TABLE #only_sql_handles ; + +IF OBJECT_ID('tempdb..#ignore_sql_handles') IS NOT NULL + DROP TABLE #ignore_sql_handles ; + +IF OBJECT_ID('tempdb..#p') IS NOT NULL + DROP TABLE #p; + +IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL + DROP TABLE #checkversion; + +IF OBJECT_ID ('tempdb..#configuration') IS NOT NULL + DROP TABLE #configuration; + +IF OBJECT_ID ('tempdb..#stored_proc_info') IS NOT NULL + DROP TABLE #stored_proc_info; + +IF OBJECT_ID ('tempdb..#plan_creation') IS NOT NULL + DROP TABLE #plan_creation; + +IF OBJECT_ID ('tempdb..#est_rows') IS NOT NULL + DROP TABLE #est_rows; + +IF OBJECT_ID ('tempdb..#plan_cost') IS NOT NULL + DROP TABLE #plan_cost; + +IF OBJECT_ID ('tempdb..#proc_costs') IS NOT NULL + DROP TABLE #proc_costs; + +IF OBJECT_ID ('tempdb..#stats_agg') IS NOT NULL + DROP TABLE #stats_agg; + +IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL + DROP TABLE #trace_flags; + +IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL + DROP TABLE #variable_info; + +IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL + DROP TABLE #conversion_info; + +IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL + DROP TABLE #missing_index_xml; + +IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL + DROP TABLE #missing_index_schema; + +IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL + DROP TABLE #missing_index_usage; + +IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL + DROP TABLE #missing_index_detail; + +IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL + DROP TABLE #missing_index_pretty; + +IF OBJECT_ID('tempdb..#index_spool_ugly') IS NOT NULL + DROP TABLE #index_spool_ugly; + +IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + +IF OBJECT_ID('tempdb..#plan_usage') IS NOT NULL + DROP TABLE #plan_usage; + +CREATE TABLE #only_query_hashes ( + query_hash BINARY(8) +); + +CREATE TABLE #ignore_query_hashes ( + query_hash BINARY(8) +); + +CREATE TABLE #only_sql_handles ( + sql_handle VARBINARY(64) +); + +CREATE TABLE #ignore_sql_handles ( + sql_handle VARBINARY(64) +); + +CREATE TABLE #p ( + SqlHandle VARBINARY(64), + TotalCPU BIGINT, + TotalDuration BIGINT, + TotalReads BIGINT, + TotalWrites BIGINT, + ExecutionCount BIGINT +); + +CREATE TABLE #checkversion ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) +); + +CREATE TABLE #configuration ( + parameter_name VARCHAR(100), + value DECIMAL(38,0) +); + +CREATE TABLE #plan_creation +( + percent_24 DECIMAL(5, 2), + percent_4 DECIMAL(5, 2), + percent_1 DECIMAL(5, 2), + total_plans INT, + SPID INT +); + +CREATE TABLE #est_rows +( + QueryHash BINARY(8), + estimated_rows FLOAT +); + +CREATE TABLE #plan_cost +( + QueryPlanCost FLOAT, + SqlHandle VARBINARY(64), + PlanHandle VARBINARY(64), + QueryHash BINARY(8), + QueryPlanHash BINARY(8) +); + +CREATE TABLE #proc_costs +( + PlanTotalQuery FLOAT, + PlanHandle VARBINARY(64), + SqlHandle VARBINARY(64) +); + +CREATE TABLE #stats_agg +( + SqlHandle VARBINARY(64), + LastUpdate DATETIME2(7), + ModificationCount BIGINT, + SamplingPercent FLOAT, + [Statistics] NVARCHAR(258), + [Table] NVARCHAR(258), + [Schema] NVARCHAR(258), + [Database] NVARCHAR(258), +); + +CREATE TABLE #trace_flags +( + SqlHandle VARBINARY(64), + QueryHash BINARY(8), + global_trace_flags VARCHAR(1000), + session_trace_flags VARCHAR(1000) +); + +CREATE TABLE #stored_proc_info +( + SPID INT, + SqlHandle VARBINARY(64), + QueryHash BINARY(8), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + converted_column_name NVARCHAR(258), + compile_time_value NVARCHAR(258), + proc_name NVARCHAR(1000), + column_name NVARCHAR(4000), + converted_to NVARCHAR(258), + set_options NVARCHAR(1000) +); + +CREATE TABLE #variable_info +( + SPID INT, + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + proc_name NVARCHAR(1000), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + compile_time_value NVARCHAR(258) +); + +CREATE TABLE #conversion_info +( + SPID INT, + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + proc_name NVARCHAR(258), + expression NVARCHAR(4000), + at_charindex AS CHARINDEX('@', expression), + bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), + comma_charindex AS CHARINDEX(',', expression) + 1, + second_comma_charindex AS + CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, + equal_charindex AS CHARINDEX('=', expression) + 1, + paren_charindex AS CHARINDEX('(', expression) + 1, + comma_paren_charindex AS + CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, + convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression) +); + + +CREATE TABLE #missing_index_xml +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + index_xml XML +); + + +CREATE TABLE #missing_index_schema +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + index_xml XML +); + + +CREATE TABLE #missing_index_usage +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + usage NVARCHAR(128), + index_xml XML +); + + +CREATE TABLE #missing_index_detail +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + usage NVARCHAR(128), + column_name NVARCHAR(128) +); + + +CREATE TABLE #missing_index_pretty +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128), + is_spool BIT, + details AS N'/* ' + + CHAR(10) + + CASE is_spool + WHEN 0 + THEN N'The Query Processor estimates that implementing the ' + ELSE N'We estimate that implementing the ' + END + + N'following index could improve query cost (' + query_cost + N')' + + CHAR(10) + + N'by ' + + CONVERT(NVARCHAR(30), impact) + + N'% for ' + executions + N' executions of the query' + + N' over the last ' + + CASE WHEN creation_hours < 24 + THEN creation_hours + N' hours.' + WHEN creation_hours = 24 + THEN ' 1 day.' + WHEN creation_hours > 24 + THEN (CONVERT(NVARCHAR(128), creation_hours / 24)) + N' days.' + ELSE N'' + END + + CHAR(10) + + N'*/' + + CHAR(10) + CHAR(13) + + N'/* ' + + CHAR(10) + + N'USE ' + + database_name + + CHAR(10) + + N'GO' + + CHAR(10) + CHAR(13) + + N'CREATE NONCLUSTERED INDEX ix_' + + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') + + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') + + CASE WHEN [include] IS NOT NULL THEN + N'_Includes' ELSE N'' END + + CHAR(10) + + N' ON ' + + schema_name + + N'.' + + table_name + + N' (' + + + CASE WHEN equality IS NOT NULL + THEN equality + + CASE WHEN inequality IS NOT NULL + THEN N', ' + inequality + ELSE N'' + END + ELSE inequality + END + + N')' + + CHAR(10) + + CASE WHEN include IS NOT NULL + THEN N'INCLUDE (' + include + N') WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + ELSE N' WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + END + + CHAR(10) + + N'GO' + + CHAR(10) + + N'*/' +); + + +CREATE TABLE #index_spool_ugly +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128) +); + + +CREATE TABLE #ReadableDBs +( +database_id INT +); + + +CREATE TABLE #plan_usage +( + duplicate_plan_hashes BIGINT NULL, + percent_duplicate DECIMAL(9, 2) NULL, + single_use_plan_count BIGINT NULL, + percent_single DECIMAL(9, 2) NULL, + total_plans BIGINT NULL, + spid INT +); + + +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; + + EXEC('INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME OPTION (RECOMPILE);'); + EXEC('INSERT INTO #ReadableDBs VALUES (32767) ;'); -- Exclude internal resource database as well +END + +RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; +WITH x AS ( +SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], + SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 4 THEN 1 ELSE 0 END) AS [plans_4], + SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 1 THEN 1 ELSE 0 END) AS [plans_1], + COUNT(deqs.creation_time) AS [total_plans] +FROM sys.dm_exec_query_stats AS deqs +) +INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) +SELECT CONVERT(DECIMAL(5,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], + x.total_plans, + @@SPID AS SPID +FROM x +OPTION (RECOMPILE); + + +RAISERROR(N'Checking for single use plans and plans with many queries', 0, 1) WITH NOWAIT; +WITH total_plans AS +( + SELECT + COUNT_BIG(deqs.query_plan_hash) AS total_plans + FROM sys.dm_exec_query_stats AS deqs +), + many_plans AS +( + SELECT + SUM(x.duplicate_plan_hashes) AS duplicate_plan_hashes + FROM + ( + SELECT + COUNT_BIG(qs.query_plan_hash) AS duplicate_plan_hashes + FROM sys.dm_exec_query_stats qs + LEFT JOIN sys.dm_exec_procedure_stats ps ON qs.plan_handle = ps.plan_handle + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = N'dbid' + AND pa.value <> 32767 /*Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/ + AND qs.query_plan_hash <> 0x0000000000000000 + GROUP BY + /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */ + qs.query_hash, + ps.object_id, + pa.value + HAVING COUNT_BIG(qs.query_plan_hash) > 5 + ) AS x +), + single_use_plans AS +( + SELECT + COUNT_BIG(*) AS single_use_plan_count + FROM sys.dm_exec_query_stats AS s + WHERE s.execution_count = 1 +) +INSERT + #plan_usage +( + duplicate_plan_hashes, + percent_duplicate, + single_use_plan_count, + percent_single, + total_plans, + spid +) +SELECT + m.duplicate_plan_hashes, + CONVERT + ( + decimal(5,2), + m.duplicate_plan_hashes + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_duplicate, + s.single_use_plan_count, + CONVERT + ( + decimal(5,2), + s.single_use_plan_count + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_single, + t.total_plans, + @@SPID +FROM many_plans AS m +CROSS JOIN single_use_plans AS s +CROSS JOIN total_plans AS t; + + +/* +Erik Darling: + Quoting this out to see if the above query fixes the issue + 2021-05-17, Issue #2909 + +UPDATE #plan_usage + SET percent_duplicate = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END, + percent_single = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END; +*/ + +SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; +SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; +SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; + +DECLARE @individual VARCHAR(100) ; + +IF (@OnlySqlHandles IS NOT NULL AND @IgnoreSqlHandles IS NOT NULL) +BEGIN +RAISERROR('You shouldn''t need to ignore and filter on SqlHandle at the same time.', 0, 1) WITH NOWAIT; +RETURN; +END; + +IF (@StoredProcName IS NOT NULL AND (@OnlySqlHandles IS NOT NULL OR @IgnoreSqlHandles IS NOT NULL)) +BEGIN +RAISERROR('You can''t filter on stored procedure name and SQL Handle.', 0, 1) WITH NOWAIT; +RETURN; +END; + +IF @OnlySqlHandles IS NOT NULL + AND LEN(@OnlySqlHandles) > 0 +BEGIN + RAISERROR(N'Processing SQL Handles', 0, 1) WITH NOWAIT; + SET @individual = ''; + + WHILE LEN(@OnlySqlHandles) > 0 + BEGIN + IF PATINDEX('%,%', @OnlySqlHandles) > 0 + BEGIN + SET @individual = SUBSTRING(@OnlySqlHandles, 0, PATINDEX('%,%',@OnlySqlHandles)) ; + + INSERT INTO #only_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); + + SET @OnlySqlHandles = SUBSTRING(@OnlySqlHandles, LEN(@individual + ',') + 1, LEN(@OnlySqlHandles)) ; + END; + ELSE + BEGIN + SET @individual = @OnlySqlHandles; + SET @OnlySqlHandles = NULL; + + INSERT INTO #only_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; + +IF @IgnoreSqlHandles IS NOT NULL + AND LEN(@IgnoreSqlHandles) > 0 +BEGIN + RAISERROR(N'Processing SQL Handles To Ignore', 0, 1) WITH NOWAIT; + SET @individual = ''; + + WHILE LEN(@IgnoreSqlHandles) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreSqlHandles) > 0 + BEGIN + SET @individual = SUBSTRING(@IgnoreSqlHandles, 0, PATINDEX('%,%',@IgnoreSqlHandles)) ; + + INSERT INTO #ignore_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); + + SET @IgnoreSqlHandles = SUBSTRING(@IgnoreSqlHandles, LEN(@individual + ',') + 1, LEN(@IgnoreSqlHandles)) ; + END; + ELSE + BEGIN + SET @individual = @IgnoreSqlHandles; + SET @IgnoreSqlHandles = NULL; + + INSERT INTO #ignore_sql_handles + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; + +IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' + +BEGIN + RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; + + DECLARE @function_search_sql NVARCHAR(MAX) = N'' + + INSERT #only_sql_handles + ( sql_handle ) + SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + FROM sys.dm_exec_procedure_stats AS deps + WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName + + UNION ALL + + SELECT ISNULL(dets.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + FROM sys.dm_exec_trigger_stats AS dets + WHERE OBJECT_NAME(dets.object_id, dets.database_id) = @StoredProcName + OPTION (RECOMPILE); + + IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_search_sql = @function_search_sql + N' + SELECT ISNULL(defs.sql_handle, CONVERT(VARBINARY(64),''0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'')) + FROM sys.dm_exec_function_stats AS defs + WHERE OBJECT_NAME(defs.object_id, defs.database_id) = @i_StoredProcName + OPTION (RECOMPILE); + ' + INSERT #only_sql_handles ( sql_handle ) + EXEC sys.sp_executesql @function_search_sql, N'@i_StoredProcName NVARCHAR(128)', @StoredProcName + END + + IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 + BEGIN + RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; + RETURN; + END; + +END; + + + +IF ((@OnlyQueryHashes IS NOT NULL AND LEN(@OnlyQueryHashes) > 0) + OR (@IgnoreQueryHashes IS NOT NULL AND LEN(@IgnoreQueryHashes) > 0)) + AND LEFT(@QueryFilter, 3) IN ('pro', 'fun') +BEGIN + RAISERROR('You cannot limit by query hash and filter by stored procedure', 16, 1); + RETURN; +END; + +/* If the user is attempting to limit by query hash, set up the + #only_query_hashes temp table. This will be used to narrow down + results. + + Just a reminder: Using @OnlyQueryHashes will ignore stored + procedures and triggers. + */ +IF @OnlyQueryHashes IS NOT NULL + AND LEN(@OnlyQueryHashes) > 0 +BEGIN + RAISERROR(N'Setting up filter for Query Hashes', 0, 1) WITH NOWAIT; + SET @individual = ''; + + WHILE LEN(@OnlyQueryHashes) > 0 + BEGIN + IF PATINDEX('%,%', @OnlyQueryHashes) > 0 + BEGIN + SET @individual = SUBSTRING(@OnlyQueryHashes, 0, PATINDEX('%,%',@OnlyQueryHashes)) ; + + INSERT INTO #only_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); + + SET @OnlyQueryHashes = SUBSTRING(@OnlyQueryHashes, LEN(@individual + ',') + 1, LEN(@OnlyQueryHashes)) ; + END; + ELSE + BEGIN + SET @individual = @OnlyQueryHashes; + SET @OnlyQueryHashes = NULL; + + INSERT INTO #only_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; + END; + END; +END; + +/* If the user is setting up a list of query hashes to ignore, those + values will be inserted into #ignore_query_hashes. This is used to + exclude values from query results. + + Just a reminder: Using @IgnoreQueryHashes will ignore stored + procedures and triggers. + */ +IF @IgnoreQueryHashes IS NOT NULL + AND LEN(@IgnoreQueryHashes) > 0 +BEGIN + RAISERROR(N'Setting up filter to ignore query hashes', 0, 1) WITH NOWAIT; + SET @individual = '' ; + + WHILE LEN(@IgnoreQueryHashes) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreQueryHashes) > 0 + BEGIN + SET @individual = SUBSTRING(@IgnoreQueryHashes, 0, PATINDEX('%,%',@IgnoreQueryHashes)) ; + + INSERT INTO #ignore_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + + SET @IgnoreQueryHashes = SUBSTRING(@IgnoreQueryHashes, LEN(@individual + ',') + 1, LEN(@IgnoreQueryHashes)) ; + END; + ELSE + BEGIN + SET @individual = @IgnoreQueryHashes ; + SET @IgnoreQueryHashes = NULL ; + + INSERT INTO #ignore_query_hashes + SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') + FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) + OPTION (RECOMPILE) ; + END; + END; +END; + +IF @ConfigurationDatabaseName IS NOT NULL +BEGIN + RAISERROR(N'Reading values from Configuration Database', 0, 1) WITH NOWAIT; + DECLARE @config_sql NVARCHAR(MAX) = N'INSERT INTO #configuration SELECT parameter_name, value FROM ' + + QUOTENAME(@ConfigurationDatabaseName) + + '.' + QUOTENAME(@ConfigurationSchemaName) + + '.' + QUOTENAME(@ConfigurationTableName) + + ' ; ' ; + EXEC(@config_sql); +END; + +RAISERROR(N'Setting up variables', 0, 1) WITH NOWAIT; +DECLARE @sql NVARCHAR(MAX) = N'', + @insert_list NVARCHAR(MAX) = N'', + @plans_triggers_select_list NVARCHAR(MAX) = N'', + @body NVARCHAR(MAX) = N'', + @body_where NVARCHAR(MAX) = N'WHERE 1 = 1 ' + @nl, + @body_order NVARCHAR(MAX) = N'ORDER BY #sortable# DESC OPTION (RECOMPILE) ', + + @q NVARCHAR(1) = N'''', + @pv VARCHAR(20), + @pos TINYINT, + @v DECIMAL(6,2), + @build INT; + + +RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; + +INSERT INTO #checkversion (version) +SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) +OPTION (RECOMPILE); + + +SELECT @v = common_version , + @build = build +FROM #checkversion +OPTION (RECOMPILE); + +IF (@SortOrder IN ('memory grant', 'avg memory grant')) AND @VersionShowsMemoryGrants = 0 +BEGIN + RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); + RETURN; +END; + +IF (@SortOrder IN ('spills', 'avg spills') AND @VersionShowsSpills = 0) +BEGIN + RAISERROR('Your version of SQL does not support sorting by spills. Please use another sort order.', 16, 1); + RETURN; +END; + +IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) +BEGIN + RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); + RETURN; +END; + +RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; + +SET @insert_list += N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO ##BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, + PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, + ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, + LastExecutionTime, LastCompletionTime, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, + LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, + QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, + TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, + min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime, Pattern) ' ; + +SET @body += N' +FROM (SELECT TOP (@Top) x.*, xpa.*, + CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY) as age_minutes, + CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY) as age_minutes_lifetime + FROM sys.#view# x + CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa + WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; + +IF @SortOrder = 'duplicate' /* Issue #3345 */ + BEGIN + SET @body += N' INNER JOIN #duplicate_query_filter AS dqf ON x.sql_handle = dqf.sql_handle AND x.plan_handle = dqf.plan_handle AND x.creation_time = dqf.duplicate_creation_time ' + @nl ; + END + +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(x.plan_handle) AS deqps ' + @nl ; + END + +SET @body += N' WHERE 1 = 1 ' + @nl ; + + 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 ; + END + +IF @IgnoreSystemDBs = 1 + BEGIN + RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; + SET @body += N' AND COALESCE(LOWER(DB_NAME(CAST(xpa.value AS INT))), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + END; + +IF @DatabaseName IS NOT NULL OR @DatabaseName <> N'' + BEGIN + RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; + SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(N' + + QUOTENAME(@DatabaseName, N'''') + + N') ' + @nl; + END; + +IF (SELECT COUNT(*) FROM #only_sql_handles) > 0 +BEGIN + RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; + SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; +END; + +IF (SELECT COUNT(*) FROM #ignore_sql_handles) > 0 +BEGIN + RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; + SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; +END; + +IF (SELECT COUNT(*) FROM #only_query_hashes) > 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #only_sql_handles) = 0 + AND (SELECT COUNT(*) FROM #ignore_sql_handles) = 0 +BEGIN + RAISERROR(N'Including only chosen Query Hashes', 0, 1) WITH NOWAIT; + SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_query_hashes q WHERE q.query_hash = x.query_hash) ' + @nl ; +END; + +/* filtering for query hashes */ +IF (SELECT COUNT(*) FROM #ignore_query_hashes) > 0 + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 +BEGIN + RAISERROR(N'Excluding chosen Query Hashes', 0, 1) WITH NOWAIT; + SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_query_hashes iq WHERE iq.query_hash = x.query_hash) ' + @nl ; +END; +/* end filtering for query hashes */ + +IF @DurationFilter IS NOT NULL + BEGIN + RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; + SET @body += N' AND (total_elapsed_time / 1000.0) / execution_count > @min_duration ' + @nl ; + END; + +IF @MinutesBack IS NOT NULL + BEGIN + RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; + SET @body += N' AND DATEADD(SECOND, (x.last_elapsed_time / 1000000.), x.last_execution_time) >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; + END; + +IF @SlowlySearchPlansFor IS NOT NULL + BEGIN + RAISERROR(N'Setting string search for @SlowlySearchPlansFor, so remember, this is gonna be slow', 0, 1) WITH NOWAIT; + SET @SlowlySearchPlansFor = REPLACE((REPLACE((REPLACE((REPLACE(@SlowlySearchPlansFor, N'[', N'_')), N']', N'_')), N'^', N'_')), N'''', N''''''); + SET @body_where += N' AND CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE N''%' + @SlowlySearchPlansFor + N'%'' ' + @nl; + END + + +/* Apply the sort order here to only grab relevant plans. + This should make it faster to process since we'll be pulling back fewer + plans for processing. + */ +RAISERROR(N'Applying chosen sort order', 0, 1) WITH NOWAIT; +SELECT @body += N' ORDER BY ' + + CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' + WHEN N'reads' THEN N'total_logical_reads' + WHEN N'writes' THEN N'total_logical_writes' + WHEN N'duration' THEN N'total_elapsed_time' + WHEN N'executions' THEN N'execution_count' + WHEN N'compiles' THEN N'cached_time' + WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'total_worker_time / execution_count' + WHEN N'avg reads' THEN N'total_logical_reads / execution_count' + WHEN N'avg writes' THEN N'total_logical_writes / execution_count' + WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' + WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, GETDATE()) + ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 + THEN DATEDIFF(mi, cached_time, last_execution_time) + ELSE Null END) as MONEY))) AS money) + END ' + END + N' DESC ' + @nl ; + + + +SET @body += N') AS qs + CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, + SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, + SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, + SUM(CAST(total_logical_reads AS DECIMAL(30))) AS t_TotalReads, + SUM(CAST(total_logical_writes AS DECIMAL(30))) AS t_TotalWrites + FROM sys.#view#) AS t + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa + CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; + +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(qs.plan_handle) AS deqps ' + @nl ; + END + +SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; + +IF @NoobSaibot = 1 +BEGIN + SET @body_where += N' AND qp.query_plan.exist(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";//p:StmtSimple//p:MissingIndex'') = 1' + @nl ; +END + +SET @plans_triggers_select_list += N' +SELECT TOP (@Top) + @@SPID , + ''Procedure or Function: '' + + QUOTENAME(COALESCE(OBJECT_SCHEMA_NAME(qs.object_id, qs.database_id),'''')) + + ''.'' + + QUOTENAME(COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''')) AS QueryType, + COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), N''-- N/A --'') AS DatabaseName, + (total_worker_time / 1000.0) / execution_count AS AvgCPU , + (total_worker_time / 1000.0) AS TotalCPU , + CASE WHEN total_worker_time = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time)) AS MONEY) + END AS AverageCPUPerMinute , + CASE WHEN t.t_TotalWorker = 0 THEN 0 + ELSE CAST(ROUND(100.00 * (total_worker_time / 1000.0) / t.t_TotalWorker, 2) AS MONEY) + END AS PercentCPUByType, + CASE WHEN t.t_TotalElapsed = 0 THEN 0 + ELSE CAST(ROUND(100.00 * (total_elapsed_time / 1000.0) / t.t_TotalElapsed, 2) AS MONEY) + END AS PercentDurationByType, + CASE WHEN t.t_TotalReads = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) + END AS PercentReadsByType, + CASE WHEN t.t_TotalExecs = 0 THEN 0 + ELSE CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) + END AS PercentExecutionsByType, + (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , + (total_elapsed_time / 1000.0) AS TotalDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + execution_count AS ExecutionCount , + CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time))) AS money) + END AS ExecutionsPerMinute , + total_logical_writes AS TotalWrites , + total_logical_writes / execution_count AS AverageWrites , + CASE WHEN t.t_TotalWrites = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) + END AS PercentWritesByType, + CASE WHEN total_logical_writes = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0)) AS money) + END AS WritesPerMinute, + qs.cached_time AS PlanCreationTime, + qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, + NULL AS StatementStartOffset, + NULL AS StatementEndOffset, + NULL AS PlanGenerationNum, + NULL AS MinReturnedRows, + NULL AS MaxReturnedRows, + NULL AS AvgReturnedRows, + NULL AS TotalReturnedRows, + NULL AS LastReturnedRows, + NULL AS MinGrantKB, + NULL AS MaxGrantKB, + NULL AS MinUsedGrantKB, + NULL AS MaxUsedGrantKB, + NULL AS PercentMemoryGrantUsed, + NULL AS AvgMaxMemoryGrant,'; + + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, '; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; + + SET @plans_triggers_select_list += + N'st.text AS QueryText ,'; + + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @plans_triggers_select_list += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END; + ELSE + BEGIN + SET @plans_triggers_select_list += N' qp.query_plan AS QueryPlan, ' + @nl ; + END; + + SET @plans_triggers_select_list += + N't.t_TotalWorker, + t.t_TotalElapsed, + t.t_TotalReads, + t.t_TotalExecs, + t.t_TotalWrites, + qs.sql_handle AS SqlHandle, + qs.plan_handle AS PlanHandle, + NULL AS QueryHash, + NULL AS QueryPlanHash, + qs.min_worker_time / 1000.0, + qs.max_worker_time / 1000.0, + CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, + qs.min_elapsed_time / 1000.0, + qs.max_elapsed_time / 1000.0, + age_minutes, + age_minutes_lifetime, + @SortOrder '; + + +IF LEFT(@QueryFilter, 3) IN ('all', 'sta') +BEGIN + SET @sql += @insert_list; + + SET @sql += N' + SELECT TOP (@Top) + @@SPID , + ''Statement'' AS QueryType, + COALESCE(DB_NAME(CAST(pa.value AS INT)), N''-- N/A --'') AS DatabaseName, + (total_worker_time / 1000.0) / execution_count AS AvgCPU , + (total_worker_time / 1000.0) AS TotalCPU , + CASE WHEN total_worker_time = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time)) AS MONEY) + END AS AverageCPUPerMinute , + CASE WHEN t.t_TotalWorker = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_worker_time / t.t_TotalWorker, 2) AS MONEY) + END AS PercentCPUByType, + CASE WHEN t.t_TotalElapsed = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_elapsed_time / t.t_TotalElapsed, 2) AS MONEY) + END AS PercentDurationByType, + CASE WHEN t.t_TotalReads = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) + END AS PercentReadsByType, + CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) AS PercentExecutionsByType, + (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , + (total_elapsed_time / 1000.0) AS TotalDuration , + total_logical_reads / execution_count AS AvgReads , + total_logical_reads AS TotalReads , + execution_count AS ExecutionCount , + CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time))) AS money) + END AS ExecutionsPerMinute , + total_logical_writes AS TotalWrites , + total_logical_writes / execution_count AS AverageWrites , + CASE WHEN t.t_TotalWrites = 0 THEN 0 + ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) + END AS PercentWritesByType, + CASE WHEN total_logical_writes = 0 THEN 0 + WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 + ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0)) AS money) + END AS WritesPerMinute, + qs.creation_time AS PlanCreationTime, + qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, + qs.statement_start_offset AS StatementStartOffset, + qs.statement_end_offset AS StatementEndOffset, + qs.plan_generation_num AS PlanGenerationNum, '; + + IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) + BEGIN + RAISERROR(N'Adding additional info columns for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + qs.min_rows AS MinReturnedRows, + qs.max_rows AS MaxReturnedRows, + CAST(qs.total_rows as MONEY) / execution_count AS AvgReturnedRows, + qs.total_rows AS TotalReturnedRows, + qs.last_rows AS LastReturnedRows, ' ; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for more info columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinReturnedRows, + NULL AS MaxReturnedRows, + NULL AS AvgReturnedRows, + NULL AS TotalReturnedRows, + NULL AS LastReturnedRows, ' ; + END; + + IF @VersionShowsMemoryGrants = 1 + BEGIN + RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + min_grant_kb AS MinGrantKB, + max_grant_kb AS MaxGrantKB, + min_used_grant_kb AS MinUsedGrantKB, + max_used_grant_kb AS MaxUsedGrantKB, + CAST(ISNULL(NULLIF(( total_used_grant_kb * 1.00 ), 0) / NULLIF(total_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, + CAST(ISNULL(NULLIF(( total_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for memory grant columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinGrantKB, + NULL AS MaxGrantKB, + NULL AS MinUsedGrantKB, + NULL AS MaxUsedGrantKB, + NULL AS PercentMemoryGrantUsed, + NULL AS AvgMaxMemoryGrant, ' ; + END; + + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills,'; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; + + SET @sql += N' + SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qs.statement_end_offset + END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , ' + @nl ; + + + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @sql += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END + ELSE + BEGIN + SET @sql += N' query_plan AS QueryPlan, ' + @nl ; + END + + SET @sql += N' + t.t_TotalWorker, + t.t_TotalElapsed, + t.t_TotalReads, + t.t_TotalExecs, + t.t_TotalWrites, + qs.sql_handle AS SqlHandle, + qs.plan_handle AS PlanHandle, + qs.query_hash AS QueryHash, + qs.query_plan_hash AS QueryPlanHash, + qs.min_worker_time / 1000.0, + qs.max_worker_time / 1000.0, + CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, + qs.min_elapsed_time / 1000.0, + qs.max_worker_time / 1000.0, + age_minutes, + age_minutes_lifetime, + @SortOrder '; + + SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; + + SET @sort_filter += CASE @SortOrder WHEN N'cpu' THEN N'AND total_worker_time > 0' + WHEN N'reads' THEN N'AND total_logical_reads > 0' + WHEN N'writes' THEN N'AND total_logical_writes > 0' + WHEN N'duration' THEN N'AND total_elapsed_time > 0' + WHEN N'executions' THEN N'AND execution_count > 0' + /* WHEN N'compiles' THEN N'AND (age_minutes + age_minutes_lifetime) > 0' BGO 2021-01-24 commenting out for https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2772 */ + WHEN N'memory grant' THEN N'AND max_grant_kb > 0' + WHEN N'unused grant' THEN N'AND max_grant_kb > 0' + WHEN N'spills' THEN N'AND max_spills > 0' + /* And now the averages */ + WHEN N'avg cpu' THEN N'AND (total_worker_time / execution_count) > 0' + WHEN N'avg reads' THEN N'AND (total_logical_reads / execution_count) > 0' + WHEN N'avg writes' THEN N'AND (total_logical_writes / execution_count) > 0' + WHEN N'avg duration' THEN N'AND (total_elapsed_time / execution_count) > 0' + WHEN N'avg memory grant' THEN N'AND CASE WHEN max_grant_kb = 0 THEN 0 ELSE (max_grant_kb / execution_count) END > 0' + WHEN N'avg spills' THEN N'AND CASE WHEN total_spills = 0 THEN 0 ELSE (total_spills / execution_count) END > 0' + WHEN N'avg executions' THEN N'AND CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END > 0' + ELSE N' /* No minimum threshold set */ ' + END; + + SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl; + + IF @SortOrder = 'compiles' + BEGIN + RAISERROR(N'Sorting by compiles', 0, 1) WITH NOWAIT; + SET @sql = REPLACE(@sql, '#sortable#', 'creation_time'); + END; +END; + + +IF (@QueryFilter = 'all' + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ + OR (LEFT(@QueryFilter, 3) = 'pro') +BEGIN + SET @sql += @insert_list; + SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Stored Procedure') ; + + SET @sql += REPLACE(@body, '#view#', 'dm_exec_procedure_stats') ; + SET @sql += @body_where ; + + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; +END; + +IF (@v >= 13 + AND @QueryFilter = 'all' + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ + AND (@SortOrder NOT IN ('spills', 'avg spills')) + OR (LEFT(@QueryFilter, 3) = 'fun') +BEGIN + SET @sql += @insert_list; + SET @sql += REPLACE(REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') + , N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, ', + N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ') ; + + SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; + SET @sql += @body_where ; + + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; +END; + +/******************************************************************************* + * + * Because the trigger execution count in SQL Server 2008R2 and earlier is not + * correct, we ignore triggers for these versions of SQL Server. If you'd like + * to include trigger numbers, just know that the ExecutionCount, + * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for + * triggers on these versions of SQL Server. + * + * This is why we can't have nice things. + * + ******************************************************************************/ +IF (@UseTriggersAnyway = 1 OR @v >= 11) + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 + AND (@QueryFilter = 'all') + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ +BEGIN + RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; + + /* Trigger level information from the plan cache */ + SET @sql += @insert_list ; + + SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; + + SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; + + SET @sql += @body_where ; + + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; +END; + + + +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' + WHEN N'reads' THEN N'total_logical_reads' + WHEN N'writes' THEN N'total_logical_writes' + WHEN N'duration' THEN N'total_elapsed_time' + WHEN N'executions' THEN N'execution_count' + WHEN N'compiles' THEN N'cached_time' + WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'total_worker_time / execution_count' + WHEN N'avg reads' THEN N'total_logical_reads / execution_count' + WHEN N'avg writes' THEN N'total_logical_writes / execution_count' + WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' + WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; + +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); + +SET @sql += N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) +SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount +FROM (SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount, + ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn + FROM ##BlitzCacheProcs + WHERE SPID = @@SPID) AS x +WHERE x.rn = 1 +OPTION (RECOMPILE); + +/* + This block was used to delete duplicate queries, but has been removed. + For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2026 +WITH d AS ( +SELECT SPID, + ROW_NUMBER() OVER (PARTITION BY SqlHandle, QueryHash ORDER BY #sortable# DESC) AS rn +FROM ##BlitzCacheProcs +WHERE SPID = @@SPID +) +DELETE d +WHERE d.rn > 1 +AND SPID = @@SPID +OPTION (RECOMPILE); +*/ +'; + +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' + WHEN N'reads' THEN N'TotalReads' + WHEN N'writes' THEN N'TotalWrites' + WHEN N'duration' THEN N'TotalDuration' + WHEN N'executions' THEN N'ExecutionCount' + WHEN N'compiles' THEN N'PlanCreationTime' + WHEN N'memory grant' THEN N'MaxGrantKB' + WHEN N'unused grant' THEN N'MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N'MaxSpills' + WHEN N'duplicate' THEN N'TotalCPU' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' + WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' + WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' + WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' + WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N'AvgSpills' + WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; + +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); + + +IF @Debug = 1 + BEGIN + PRINT N'Printing dynamic SQL stored in @sql: '; + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; + +RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; + + +IF OBJECT_ID('tempdb.dbo.##BlitzCacheResults') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheResults ( + SPID INT, + ID INT IDENTITY(1,1), + CheckID INT, + Priority TINYINT, + FindingsGroup VARCHAR(50), + Finding VARCHAR(500), + URL VARCHAR(200), + Details VARCHAR(4000) + ); +END; +ELSE +BEGIN + RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheResults + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END + + +IF OBJECT_ID('tempdb.dbo.##BlitzCacheProcs') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheProcs ( + SPID INT , + QueryType NVARCHAR(258), + DatabaseName sysname, + AverageCPU DECIMAL(38,4), + AverageCPUPerMinute DECIMAL(38,4), + TotalCPU DECIMAL(38,4), + PercentCPUByType MONEY, + PercentCPU MONEY, + AverageDuration DECIMAL(38,4), + TotalDuration DECIMAL(38,4), + PercentDuration MONEY, + PercentDurationByType MONEY, + AverageReads BIGINT, + TotalReads BIGINT, + PercentReads MONEY, + PercentReadsByType MONEY, + ExecutionCount BIGINT, + PercentExecutions MONEY, + PercentExecutionsByType MONEY, + ExecutionsPerMinute MONEY, + TotalWrites BIGINT, + AverageWrites MONEY, + PercentWrites MONEY, + PercentWritesByType MONEY, + WritesPerMinute MONEY, + PlanCreationTime DATETIME, + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' + ELSE 'N/A' END, + SqlHandle VARBINARY(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' + ELSE 'N/A' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' + ELSE 'N/A' END, + QueryHash BINARY(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' + ELSE 'N/A' END, + QueryPlanHash BINARY(8), + StatementStartOffset INT, + StatementEndOffset INT, + PlanGenerationNum BIGINT, + MinReturnedRows BIGINT, + MaxReturnedRows BIGINT, + AverageReturnedRows MONEY, + TotalReturnedRows BIGINT, + LastReturnedRows BIGINT, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryText NVARCHAR(MAX), + QueryPlan XML, + /* these next four columns are the total for the type of query. + don't actually use them for anything apart from math by type. + */ + TotalWorkerTimeForType BIGINT, + TotalElapsedTimeForType BIGINT, + TotalReadsForType BIGINT, + TotalExecutionCountForType BIGINT, + TotalWritesForType BIGINT, + NumberOfPlans INT, + NumberOfDistinctPlans INT, + SerialDesiredMemory FLOAT, + SerialRequiredMemory FLOAT, + CachedPlanSize FLOAT, + CompileTime FLOAT, + CompileCPU FLOAT , + CompileMemory FLOAT , + MaxCompileMemory FLOAT , + min_worker_time BIGINT, + max_worker_time BIGINT, + is_forced_plan BIT, + is_forced_parameterized BIT, + is_cursor BIT, + is_optimistic_cursor BIT, + is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, + is_parallel BIT, + is_forced_serial BIT, + is_key_lookup_expensive BIT, + key_lookup_cost FLOAT, + is_remote_query_expensive BIT, + remote_query_cost FLOAT, + frequent_execution BIT, + parameter_sniffing BIT, + unparameterized_query BIT, + near_parallel BIT, + plan_warnings BIT, + plan_multiple_plans INT, + long_running BIT, + downlevel_estimator BIT, + implicit_conversions BIT, + busy_loops BIT, + tvf_join BIT, + tvf_estimate BIT, + compile_timeout BIT, + compile_memory_limit_exceeded BIT, + warning_no_join_predicate BIT, + QueryPlanCost FLOAT, + missing_index_count INT, + unmatched_index_count INT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + age_minutes MONEY, + age_minutes_lifetime MONEY, + is_trivial BIT, + trace_flags_session VARCHAR(1000), + is_unused_grant BIT, + function_count INT, + clr_function_count INT, + is_table_variable BIT, + no_stats_warning BIT, + relop_warnings BIT, + is_table_scan BIT, + backwards_scan BIT, + forced_index BIT, + forced_seek BIT, + forced_scan BIT, + columnstore_row_mode BIT, + is_computed_scalar BIT , + is_sort_expensive BIT, + sort_cost FLOAT, + is_computed_filter BIT, + op_name VARCHAR(100) NULL, + index_insert_count INT NULL, + index_update_count INT NULL, + index_delete_count INT NULL, + cx_insert_count INT NULL, + cx_update_count INT NULL, + cx_delete_count INT NULL, + table_insert_count INT NULL, + table_update_count INT NULL, + table_delete_count INT NULL, + index_ops AS (index_insert_count + index_update_count + index_delete_count + + cx_insert_count + cx_update_count + cx_delete_count + + table_insert_count + table_update_count + table_delete_count), + is_row_level BIT, + is_spatial BIT, + index_dml BIT, + table_dml BIT, + long_running_low_cpu BIT, + low_cost_high_cpu BIT, + stale_stats BIT, + is_adaptive BIT, + index_spool_cost FLOAT, + index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, + is_spool_expensive BIT, + is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, + estimated_rows FLOAT, + is_bad_estimate BIT, + is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, + implicit_conversion_info XML, + cached_execution_parameters XML, + missing_indexes XML, + SetOptions VARCHAR(MAX), + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) + ); +END; +ELSE +BEGIN + RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheProcs + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END + +IF @Reanalyze = 0 +BEGIN + RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; + + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @SortOrder NVARCHAR(20)', @Top, @DurationFilter_i, @MinutesBack, @SortOrder; +END; + +IF @SkipAnalysis = 1 + BEGIN + RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; + GOTO Results ; + END; + + +/* Update ##BlitzCacheProcs to get Stored Proc info + * This should get totals for all statements in a Stored Proc + */ +RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; +;WITH agg AS ( + SELECT + b.SqlHandle, + SUM(b.MinReturnedRows) AS MinReturnedRows, + SUM(b.MaxReturnedRows) AS MaxReturnedRows, + SUM(b.AverageReturnedRows) AS AverageReturnedRows, + SUM(b.TotalReturnedRows) AS TotalReturnedRows, + SUM(b.LastReturnedRows) AS LastReturnedRows, + SUM(b.MinGrantKB) AS MinGrantKB, + SUM(b.MaxGrantKB) AS MaxGrantKB, + SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, + SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB, + SUM(b.MinSpills) AS MinSpills, + SUM(b.MaxSpills) AS MaxSpills, + SUM(b.TotalSpills) AS TotalSpills + FROM ##BlitzCacheProcs b + WHERE b.SPID = @@SPID + AND b.QueryHash IS NOT NULL + GROUP BY b.SqlHandle +) +UPDATE b + SET + b.MinReturnedRows = b2.MinReturnedRows, + b.MaxReturnedRows = b2.MaxReturnedRows, + b.AverageReturnedRows = b2.AverageReturnedRows, + b.TotalReturnedRows = b2.TotalReturnedRows, + b.LastReturnedRows = b2.LastReturnedRows, + b.MinGrantKB = b2.MinGrantKB, + b.MaxGrantKB = b2.MaxGrantKB, + b.MinUsedGrantKB = b2.MinUsedGrantKB, + b.MaxUsedGrantKB = b2.MaxUsedGrantKB, + b.MinSpills = b2.MinSpills, + b.MaxSpills = b2.MaxSpills, + b.TotalSpills = b2.TotalSpills +FROM ##BlitzCacheProcs b +JOIN agg b2 +ON b2.SqlHandle = b.SqlHandle +WHERE b.QueryHash IS NULL +AND b.SPID = @@SPID +OPTION (RECOMPILE) ; + +/* Compute the total CPU, etc across our active set of the plan cache. + * Yes, there's a flaw - this doesn't include anything outside of our @Top + * metric. + */ +RAISERROR('Computing CPU, duration, read, and write metrics', 0, 1) WITH NOWAIT; +DECLARE @total_duration BIGINT, + @total_cpu BIGINT, + @total_reads BIGINT, + @total_writes BIGINT, + @total_execution_count BIGINT; + +SELECT @total_cpu = SUM(TotalCPU), + @total_duration = SUM(TotalDuration), + @total_reads = SUM(TotalReads), + @total_writes = SUM(TotalWrites), + @total_execution_count = SUM(ExecutionCount) +FROM #p +OPTION (RECOMPILE) ; + +DECLARE @cr NVARCHAR(1) = NCHAR(13); +DECLARE @lf NVARCHAR(1) = NCHAR(10); +DECLARE @tab NVARCHAR(1) = NCHAR(9); + +/* Update CPU percentage for stored procedures */ +RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET PercentCPU = y.PercentCPU, + PercentDuration = y.PercentDuration, + PercentReads = y.PercentReads, + PercentWrites = y.PercentWrites, + PercentExecutions = y.PercentExecutions, + ExecutionsPerMinute = y.ExecutionsPerMinute, + /* Strip newlines and tabs. Tabs are replaced with multiple spaces + so that the later whitespace trim will completely eliminate them + */ + 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 + ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, + CASE @total_duration WHEN 0 THEN 0 + ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, + CASE @total_reads WHEN 0 THEN 0 + ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, + CASE @total_writes WHEN 0 THEN 0 + ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, + CASE @total_execution_count WHEN 0 THEN 0 + ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, + CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) + WHEN 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) + END AS ExecutionsPerMinute + FROM ( + SELECT PlanHandle, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + FROM ##BlitzCacheProcs + WHERE PlanHandle IS NOT NULL + AND SPID = @@SPID + GROUP BY PlanHandle, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + ) AS x +) AS y +WHERE ##BlitzCacheProcs.PlanHandle = y.PlanHandle + AND ##BlitzCacheProcs.PlanHandle IS NOT NULL + AND ##BlitzCacheProcs.SPID = @@SPID +OPTION (RECOMPILE) ; + + +RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET PercentCPU = y.PercentCPU, + PercentDuration = y.PercentDuration, + PercentReads = y.PercentReads, + PercentWrites = y.PercentWrites, + PercentExecutions = y.PercentExecutions, + ExecutionsPerMinute = y.ExecutionsPerMinute, + /* Strip newlines and tabs. Tabs are replaced with multiple spaces + so that the later whitespace trim will completely eliminate them + */ + QueryText = CASE WHEN @KeepCRLF = 1 + THEN REPLACE(QueryText, @tab, ' ') + ELSE REPLACE(REPLACE(REPLACE(QueryText, @cr, ' '), @lf, ' '), @tab, ' ') + END +FROM ( + SELECT DatabaseName, + SqlHandle, + QueryHash, + CASE @total_cpu WHEN 0 THEN 0 + ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, + CASE @total_duration WHEN 0 THEN 0 + ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, + CASE @total_reads WHEN 0 THEN 0 + ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, + CASE @total_writes WHEN 0 THEN 0 + ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, + CASE @total_execution_count WHEN 0 THEN 0 + ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, + CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) + WHEN 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) + END AS ExecutionsPerMinute + FROM ( + SELECT DatabaseName, + SqlHandle, + QueryHash, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + FROM ##BlitzCacheProcs + WHERE SPID = @@SPID + GROUP BY DatabaseName, + SqlHandle, + QueryHash, + TotalCPU, + TotalDuration, + TotalReads, + TotalWrites, + ExecutionCount, + PlanCreationTime, + LastExecutionTime + ) AS x +) AS y +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle + AND ##BlitzCacheProcs.QueryHash = y.QueryHash + AND ##BlitzCacheProcs.DatabaseName = y.DatabaseName + AND ##BlitzCacheProcs.PlanHandle IS NULL +OPTION (RECOMPILE) ; + + + +/* Testing using XML nodes to speed up processing */ +RAISERROR(N'Begin XML nodes processing', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + PlanHandle, + q.n.query('.') AS statement, + 0 AS is_cursor +INTO #statements +FROM ##BlitzCacheProcs p + CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) +WHERE p.SPID = @@SPID +OPTION (RECOMPILE) ; + +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT #statements +SELECT QueryHash , + SqlHandle , + PlanHandle, + q.n.query('.') AS statement, + 1 AS is_cursor +FROM ##BlitzCacheProcs p + CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) +WHERE p.SPID = @@SPID +OPTION (RECOMPILE) ; + +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + q.n.query('.') AS query_plan +INTO #query_plan +FROM #statements p + CROSS APPLY p.statement.nodes('//p:QueryPlan') AS q(n) +OPTION (RECOMPILE) ; + +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +SELECT QueryHash , + SqlHandle , + q.n.query('.') AS relop +INTO #relop +FROM #query_plan p + CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) +OPTION (RECOMPILE) ; + +-- high level plan stuff +RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET NumberOfDistinctPlans = distinct_plan_count, + NumberOfPlans = number_of_plans , + plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN number_of_plans END +FROM + ( + SELECT + DatabaseName = + DB_NAME(CONVERT(int, pa.value)), + QueryHash = + qs.query_hash, + number_of_plans = + COUNT_BIG(qs.query_plan_hash), + distinct_plan_count = + COUNT_BIG(DISTINCT qs.query_plan_hash) + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = 'dbid' + GROUP BY + DB_NAME(CONVERT(int, pa.value)), + qs.query_hash +) AS x +WHERE ##BlitzCacheProcs.QueryHash = x.QueryHash +AND ##BlitzCacheProcs.DatabaseName = x.DatabaseName +OPTION (RECOMPILE) ; + +-- query level checks +RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , + unmatched_index_count = CASE WHEN is_trivial <> 1 THEN query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') END , + SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , + SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), + CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , + CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , + CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , + CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float'), + MaxCompileMemory = query_plan.value('sum(//p:QueryPlan/p:OptimizerHardwareDependentProperties/@MaxCompileMemory)', 'float') +FROM #query_plan qp +WHERE qp.QueryHash = ##BlitzCacheProcs.QueryHash +AND qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + +-- statement level checks +RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET compile_timeout = 1 +FROM #statements s +JOIN ##BlitzCacheProcs b +ON s.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 +OPTION (RECOMPILE); + +RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET compile_memory_limit_exceeded = 1 +FROM #statements s +JOIN ##BlitzCacheProcs b +ON s.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +unparameterized_query AS ( + SELECT s.QueryHash, + unparameterized_query = CASE WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 1 AND + statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList/p:ColumnReference') = 0 THEN 1 + WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 0 AND + statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/*/p:RelOp/descendant::p:ScalarOperator/p:Identifier/p:ColumnReference[contains(@Column, "@")]') = 1 THEN 1 + END + FROM #statements AS s + ) +UPDATE b +SET b.unparameterized_query = u.unparameterized_query +FROM ##BlitzCacheProcs b +JOIN unparameterized_query u +ON u.QueryHash = b.QueryHash +AND SPID = @@SPID +WHERE u.unparameterized_query = 1 +OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +index_dml AS ( + SELECT s.QueryHash, + index_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 + WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 + END + FROM #statements s + ) + UPDATE b + SET b.index_dml = i.index_dml + FROM ##BlitzCacheProcs AS b + JOIN index_dml i + ON i.QueryHash = b.QueryHash + WHERE i.index_dml = 1 + AND b.SPID = @@SPID + OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +table_dml AS ( + SELECT s.QueryHash, + table_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 + WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 + END + FROM #statements AS s + ) + UPDATE b + SET b.table_dml = t.table_dml + FROM ##BlitzCacheProcs AS b + JOIN table_dml t + ON t.QueryHash = b.QueryHash + WHERE t.table_dml = 1 + AND b.SPID = @@SPID + OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT INTO #est_rows +SELECT DISTINCT + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, + c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows +FROM #statements AS s +CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) +WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; + + UPDATE b + SET b.estimated_rows = er.estimated_rows + FROM ##BlitzCacheProcs AS b + JOIN #est_rows er + ON er.QueryHash = b.QueryHash + WHERE b.SPID = @@SPID + AND b.QueryType = 'Statement' + OPTION (RECOMPILE); +END; + +RAISERROR(N'Gathering trivial plans', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +UPDATE b +SET b.is_trivial = 1 +FROM ##BlitzCacheProcs AS b +JOIN ( +SELECT s.SqlHandle +FROM #statements AS s +JOIN ( SELECT r.SqlHandle + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[contains(@LogicalOp, "Scan")]') = 1 ) AS r + ON r.SqlHandle = s.SqlHandle +WHERE s.statement.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 +) AS s +ON b.SqlHandle = s.SqlHandle +OPTION (RECOMPILE); + + +--Gather costs +RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT INTO #plan_cost ( QueryPlanCost, SqlHandle, PlanHandle, QueryHash, QueryPlanHash ) +SELECT DISTINCT + statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, + s.SqlHandle, + s.PlanHandle, + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, + CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash +FROM #statements s +CROSS APPLY s.statement.nodes('/p:StmtSimple') AS q(n) +WHERE statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 +OPTION (RECOMPILE); + +RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; +WITH pc AS ( + SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle + FROM #plan_cost AS pc + GROUP BY pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle +) + UPDATE b + SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) + FROM pc + JOIN ##BlitzCacheProcs b + ON b.SqlHandle = pc.SqlHandle + AND b.QueryHash = pc.QueryHash + WHERE b.QueryType NOT LIKE '%Procedure%' + OPTION (RECOMPILE); + +IF EXISTS ( +SELECT 1 +FROM ##BlitzCacheProcs AS b +WHERE b.QueryType LIKE 'Procedure%' +) + +BEGIN + +RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, QueryCost AS ( + SELECT + DISTINCT + statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') AS SubTreeCost, + s.PlanHandle, + s.SqlHandle + FROM #statements AS s + WHERE PlanHandle IS NOT NULL +) +, QueryCostUpdate AS ( + SELECT + SUM(qc.SubTreeCost) OVER (PARTITION BY SqlHandle, PlanHandle) PlanTotalQuery, + qc.PlanHandle, + qc.SqlHandle + FROM QueryCost qc +) +INSERT INTO #proc_costs +SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle +FROM QueryCostUpdate AS qcu +OPTION (RECOMPILE); + + +UPDATE b + SET b.QueryPlanCost = ca.PlanTotalQuery +FROM ##BlitzCacheProcs AS b +CROSS APPLY ( + SELECT TOP 1 PlanTotalQuery + FROM #proc_costs qcu + WHERE qcu.PlanHandle = b.PlanHandle + ORDER BY PlanTotalQuery DESC +) ca +WHERE b.QueryType LIKE 'Procedure%' +AND b.SPID = @@SPID +OPTION (RECOMPILE); + +END; + +UPDATE b +SET b.QueryPlanCost = 0.0 +FROM ##BlitzCacheProcs b +WHERE b.QueryPlanCost IS NULL +AND b.SPID = @@SPID +OPTION (RECOMPILE); + +RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET plan_warnings = 1 +FROM #query_plan qp +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 +OPTION (RECOMPILE); + +RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET implicit_conversions = 1 +FROM #query_plan qp +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 +OPTION (RECOMPILE); + +-- operator level checks +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END +FROM ##BlitzCacheProcs p + JOIN ( + SELECT qs.SqlHandle, + relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , + relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions + FROM #relop qs + ) AS x ON p.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; + + +RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE p +SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END +FROM ##BlitzCacheProcs p + JOIN ( + SELECT r.SqlHandle, + 1 AS tvf_join + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 + AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 + ) AS x ON p.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT r.SqlHandle, + c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, + c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , + c.n.exist('//p:Warnings') AS relop_warnings +FROM #relop AS r +CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) +) +UPDATE p +SET p.warning_no_join_predicate = x.warning_no_join_predicate, + p.no_stats_warning = x.no_stats_warning, + p.relop_warnings = x.relop_warnings +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; + + +RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT r.SqlHandle, + c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char +FROM #relop r +CROSS APPLY r.relop.nodes('//p:Object') AS c(n) +) +UPDATE p +SET is_table_variable = 1 +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +WHERE x.first_char = '@' +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, x AS ( +SELECT qs.SqlHandle, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) +) +UPDATE p +SET p.function_count = x.function_count, + p.clr_function_count = x.clr_function_count +FROM ##BlitzCacheProcs AS p +JOIN x ON x.SqlHandle = p.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; + + +RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET key_lookup_cost = x.key_lookup_cost +FROM ( +SELECT + qs.SqlHandle, + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS key_lookup_cost +FROM #relop qs +WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 +GROUP BY qs.SqlHandle +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + + +RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET remote_query_cost = x.remote_query_cost +FROM ( +SELECT + qs.SqlHandle, + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS remote_query_cost +FROM #relop qs +WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 +GROUP BY qs.SqlHandle +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + +RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET sort_cost = y.max_sort_cost +FROM ( + SELECT x.SqlHandle, MAX((x.sort_io + x.sort_cpu)) AS max_sort_cost + FROM ( + SELECT + qs.SqlHandle, + relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, + relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu + FROM #relop qs + WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 + ) AS x + GROUP BY x.SqlHandle + ) AS y +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + +IF NOT EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'No cursor plans found, skipping', 0, 1) WITH NOWAIT; + +END + +IF EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'Cursor plans found, investigating', 0, 1) WITH NOWAIT; + +RAISERROR(N'Checking for Optimistic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_optimistic_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking if cursor is Forward Only', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_forward_only_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + +RAISERROR(N'Checking if cursor is Fast Forward', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_fast_forward_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="FastForward"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking for Dynamic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_cursor_dynamic = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="Dynamic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + +END + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET +b.is_table_scan = x.is_table_scan, +b.backwards_scan = x.backwards_scan, +b.forced_index = x.forced_index, +b.forced_seek = x.forced_seek, +b.forced_scan = x.forced_scan +FROM ##BlitzCacheProcs b +JOIN ( +SELECT + qs.SqlHandle, + 0 AS is_table_scan, + q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, + q.n.value('@ForcedIndex', 'bit') AS forced_index, + q.n.value('@ForceSeek', 'bit') AS forced_seek, + q.n.value('@ForceScan', 'bit') AS forced_scan +FROM #relop qs +CROSS APPLY qs.relop.nodes('//p:IndexScan') AS q(n) +UNION ALL +SELECT + qs.SqlHandle, + 1 AS is_table_scan, + q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, + q.n.value('@ForcedIndex', 'bit') AS forced_index, + q.n.value('@ForceSeek', 'bit') AS forced_seek, + q.n.value('@ForceScan', 'bit') AS forced_scan +FROM #relop qs +CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) +) AS x ON b.SqlHandle = x.SqlHandle +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_computed_scalar = x.computed_column_function +FROM ( +SELECT qs.SqlHandle, + n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) +WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; + + +RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_computed_filter = x.filter_function +FROM ( +SELECT +r.SqlHandle, +c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function +FROM #relop AS r +CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) +) x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +IndexOps AS +( + SELECT + r.QueryHash, + c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, + c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, + c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, + c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, + c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, + c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, + c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, + c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, + c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, + c.n.exist('@PhysicalOp[.="Table Delete"]') AS td + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp') c(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) +), iops AS +( + SELECT ios.QueryHash, + SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, + SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, + SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, + SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, + SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, + SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, + SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, + SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, + SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count + FROM IndexOps AS ios + WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', + 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', + 'Table Insert', 'Table Delete', 'Table Update') + GROUP BY ios.QueryHash) +UPDATE b +SET b.index_insert_count = iops.index_insert_count, + b.index_update_count = iops.index_update_count, + b.index_delete_count = iops.index_delete_count, + b.cx_insert_count = iops.cx_insert_count, + b.cx_update_count = iops.cx_update_count, + b.cx_delete_count = iops.cx_delete_count, + b.table_insert_count = iops.table_insert_count, + b.table_update_count = iops.table_update_count, + b.table_delete_count = iops.table_delete_count +FROM ##BlitzCacheProcs AS b +JOIN iops ON iops.QueryHash = b.QueryHash +WHERE SPID = @@SPID +OPTION (RECOMPILE); +END; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET is_spatial = x.is_spatial +FROM ( +SELECT qs.SqlHandle, + 1 AS is_spatial +FROM #relop qs +CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) +WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 +) AS x +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); +END; + + +RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT s.QueryHash + FROM #statements AS s + WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) +, spools +AS ( SELECT DISTINCT r.QueryHash, + c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, + c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, + c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds +FROM #relop AS r +JOIN selects AS s +ON s.QueryHash = r.QueryHash +CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) +WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 +) +UPDATE b + SET b.index_spool_rows = sp.estimated_rows, + b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b +JOIN spools sp +ON sp.QueryHash = b.QueryHash +OPTION (RECOMPILE); + +RAISERROR('Checking for wonky Table Spools', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT s.QueryHash + FROM #statements AS s + WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) +, spools +AS ( SELECT DISTINCT r.QueryHash, + c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, + c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, + c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds +FROM #relop AS r +JOIN selects AS s +ON s.QueryHash = r.QueryHash +CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) +WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Table Spool" and @LogicalOp="Lazy Spool"]') = 1 +) +UPDATE b + SET b.table_spool_rows = (sp.estimated_rows * sp.estimated_rebinds), + b.table_spool_cost = ((sp.estimated_io * sp.estimated_cpu * sp.estimated_rows) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b +JOIN spools sp +ON sp.QueryHash = b.QueryHash +OPTION (RECOMPILE); + + +RAISERROR('Checking for selects that cause non-spill and index spool writes', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT CONVERT(BINARY(8), + RIGHT('0000000000000000' + + SUBSTRING(s.statement.value('(/p:StmtSimple/@QueryHash)[1]', 'VARCHAR(18)'), + 3, 18), 16), 2) AS QueryHash + FROM #statements AS s + JOIN ##BlitzCacheProcs b + ON s.QueryHash = b.QueryHash + WHERE b.index_spool_rows IS NULL + AND b.index_spool_cost IS NULL + AND b.table_spool_cost IS NULL + AND b.table_spool_rows IS NULL + AND b.is_big_spills IS NULL + AND b.AverageWrites > 1024. + AND s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 +) +UPDATE b + SET b.select_with_writes = 1 +FROM ##BlitzCacheProcs b +JOIN selects AS s +ON s.QueryHash = b.QueryHash +AND b.AverageWrites > 1024.; + +/* 2012+ only */ +IF @v >= 11 +BEGIN + + RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE ##BlitzCacheProcs + SET is_forced_serial = 1 + FROM #query_plan qp + WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle + AND SPID = @@SPID + AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 + AND (##BlitzCacheProcs.is_parallel = 0 OR ##BlitzCacheProcs.is_parallel IS NULL) + OPTION (RECOMPILE); + + IF @ExpertMode > 0 + BEGIN + RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE ##BlitzCacheProcs + SET columnstore_row_mode = x.is_row_mode + FROM ( + SELECT + qs.SqlHandle, + relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode + FROM #relop qs + WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 + ) AS x + WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle + AND SPID = @@SPID + OPTION (RECOMPILE); + END; + +END; + +/* 2014+ only */ +IF @v >= 12 +BEGIN + RAISERROR('Checking for downlevel cardinality estimators being used on SQL Server 2014.', 0, 1) WITH NOWAIT; + + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE p + SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END + FROM ##BlitzCacheProcs p + JOIN #statements s ON p.QueryHash = s.QueryHash + WHERE SPID = @@SPID + OPTION (RECOMPILE); +END ; + +/* 2016+ only */ +IF @v >= 13 AND @ExpertMode > 0 +BEGIN + RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; + + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + UPDATE p + SET p.is_row_level = 1 + FROM ##BlitzCacheProcs p + JOIN #statements s ON p.QueryHash = s.QueryHash + WHERE SPID = @@SPID + AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 + OPTION (RECOMPILE); +END ; + +/* 2017+ only */ +IF @v >= 14 OR (@v = 13 AND @build >= 5026) +BEGIN + +IF @ExpertMode > 0 +BEGIN +RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +INSERT INTO #stats_agg +SELECT qp.SqlHandle, + x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, + x.c.value('@ModificationCount', 'BIGINT') AS ModificationCount, + x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, + x.c.value('@Statistics', 'NVARCHAR(258)') AS [Statistics], + x.c.value('@Table', 'NVARCHAR(258)') AS [Table], + x.c.value('@Schema', 'NVARCHAR(258)') AS [Schema], + x.c.value('@Database', 'NVARCHAR(258)') AS [Database] +FROM #query_plan AS qp +CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) +OPTION (RECOMPILE); + + +RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; +WITH stale_stats AS ( + SELECT sa.SqlHandle + FROM #stats_agg AS sa + GROUP BY sa.SqlHandle + HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) + AND AVG(sa.ModificationCount) >= 100000 +) +UPDATE b +SET stale_stats = 1 +FROM ##BlitzCacheProcs b +JOIN stale_stats os +ON b.SqlHandle = os.SqlHandle +AND b.SPID = @@SPID +OPTION (RECOMPILE); +END; + +IF @v >= 14 AND @ExpertMode > 0 +BEGIN +RAISERROR('Checking for adaptive joins', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +aj AS ( + SELECT + SqlHandle + FROM #relop AS r + CROSS APPLY r.relop.nodes('//p:RelOp') x(c) + WHERE x.c.exist('@IsAdaptive[.=1]') = 1 +) +UPDATE b +SET b.is_adaptive = 1 +FROM ##BlitzCacheProcs b +JOIN aj +ON b.SqlHandle = aj.SqlHandle +AND b.SPID = @@SPID +OPTION (RECOMPILE); +END; + +IF ((@v >= 14 + OR (@v = 13 AND @build >= 5026) + OR (@v = 12 AND @build >= 6024)) + AND @ExpertMode > 0) + +BEGIN; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +row_goals AS( +SELECT qs.QueryHash +FROM #relop qs +WHERE relop.value('sum(/p:RelOp/@EstimateRowsWithoutRowGoal)', 'float') > 0 +) +UPDATE b +SET b.is_row_goal = 1 +FROM ##BlitzCacheProcs b +JOIN row_goals +ON b.QueryHash = row_goals.QueryHash +AND b.SPID = @@SPID +OPTION (RECOMPILE); +END ; + +END; + + +/* END Testing using XML nodes to speed up processing */ + + +/* Update to grab stored procedure name for individual statements */ +RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; +UPDATE p +SET QueryType = QueryType + ' (parent ' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + '.' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' +FROM ##BlitzCacheProcs p + JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle +WHERE QueryType = 'Statement' +AND SPID = @@SPID +OPTION (RECOMPILE); + +/* Update to grab stored procedure name for individual statements when PSPO is detected */ +UPDATE p +SET QueryType = QueryType + ' (parent ' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + '.' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' +FROM ##BlitzCacheProcs p + OUTER APPLY ( + SELECT REPLACE(REPLACE(REPLACE(REPLACE(p.QueryText, ' (', '('), '( ', '('), ' =', '='), '= ', '=') AS NormalizedQueryText + ) a + OUTER APPLY ( + SELECT CHARINDEX('option(PLAN PER VALUE(ObjectID=', a.NormalizedQueryText) AS OptionStart + ) b + OUTER APPLY ( + SELECT SUBSTRING(a.NormalizedQueryText, b.OptionStart + 31, LEN(a.NormalizedQueryText) - b.OptionStart - 30) AS OptionSubstring + WHERE b.OptionStart > 0 + ) c + OUTER APPLY ( + SELECT PATINDEX('%[^0-9]%', c.OptionSubstring) AS ObjectLength + ) d + OUTER APPLY ( + SELECT TRY_CAST(SUBSTRING(OptionSubstring, 1, d.ObjectLength - 1) AS INT) AS ObjectId + ) e + JOIN sys.dm_exec_procedure_stats s ON DB_ID(p.DatabaseName) = s.database_id AND e.ObjectId = s.object_id +WHERE p.QueryType = 'Statement' +AND p.SPID = @@SPID +AND s.object_id IS NOT NULL +OPTION (RECOMPILE); + +RAISERROR(N'Attempting to get function name for individual statements', 0, 1) WITH NOWAIT; +DECLARE @function_update_sql NVARCHAR(MAX) = N'' +IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_update_sql = @function_update_sql + N' + UPDATE p + SET QueryType = QueryType + '' (parent '' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + ''.'' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + '')'' + FROM ##BlitzCacheProcs p + JOIN sys.dm_exec_function_stats s ON p.SqlHandle = s.sql_handle + WHERE QueryType = ''Statement'' + AND SPID = @@SPID + OPTION (RECOMPILE); + ' + EXEC sys.sp_executesql @function_update_sql + END + + +/* Trace Flag Checks 2012 SP3, 2014 SP2 and 2016 SP1 only)*/ +IF @v >= 11 +BEGIN + +RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; +;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +, tf_pretty AS ( +SELECT qp.QueryHash, + qp.SqlHandle, + q.n.value('@Value', 'INT') AS trace_flag, + q.n.value('@Scope', 'VARCHAR(10)') AS scope +FROM #query_plan qp +CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) +) +INSERT INTO #trace_flags +SELECT DISTINCT tf1.SqlHandle , tf1.QueryHash, + STUFF(( + SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) + FROM tf_pretty AS tf2 + WHERE tf1.SqlHandle = tf2.SqlHandle + AND tf1.QueryHash = tf2.QueryHash + AND tf2.scope = 'Global' + FOR XML PATH(N'')), 1, 2, N'' + ) AS global_trace_flags, + STUFF(( + SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) + FROM tf_pretty AS tf2 + WHERE tf1.SqlHandle = tf2.SqlHandle + AND tf1.QueryHash = tf2.QueryHash + AND tf2.scope = 'Session' + FOR XML PATH(N'')), 1, 2, N'' + ) AS session_trace_flags +FROM tf_pretty AS tf1 +OPTION (RECOMPILE); + +UPDATE p +SET p.trace_flags_session = tf.session_trace_flags +FROM ##BlitzCacheProcs p +JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash +WHERE SPID = @@SPID +OPTION (RECOMPILE); + +END; + + +RAISERROR(N'Checking for MSTVFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mstvf = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp[(@EstimateRows="100" or @EstimateRows="1") and @LogicalOp="Table-valued function"]') = 1 +OPTION (RECOMPILE); + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for many to many merge joins', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mm_join = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp/p:Merge/@ManyToMany[.="1"]') = 1 +OPTION (RECOMPILE); +END ; + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +is_paul_white_electric AS ( +SELECT 1 AS [is_paul_white_electric], +r.SqlHandle +FROM #relop AS r +CROSS APPLY r.relop.nodes('//p:RelOp') c(n) +WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 +) +UPDATE b +SET b.is_paul_white_electric = ipwe.is_paul_white_electric +FROM ##BlitzCacheProcs AS b +JOIN is_paul_white_electric ipwe +ON ipwe.SqlHandle = b.SqlHandle +WHERE b.SPID = @@SPID +OPTION (RECOMPILE); +END ; + + +RAISERROR(N'Checking for non-sargable predicates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, nsarg + AS ( SELECT r.QueryHash, 1 AS fn, 0 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator') AS ca(x) + WHERE ( ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName') = 1 + OR ca.x.exist('//p:ScalarOperator/p:IF') = 1 ) + UNION ALL + SELECT r.QueryHash, 0 AS fn, 1 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp//p:ScalarOperator') AS ca(x) + WHERE r.relop.exist('/p:RelOp[contains(@LogicalOp, "Join")]') = 1 + AND ca.x.exist('//p:ScalarOperator[contains(@ScalarString, "Expr")]') = 1 + UNION ALL + SELECT r.QueryHash, 0 AS fn, 0 AS jo, 1 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator') AS ca(x) + CROSS APPLY ca.x.nodes('//p:Const') AS co(x) + WHERE ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName[.="like"]') = 1 + AND ( ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') <> 'N' + AND co.x.value('substring(@ConstValue, 2, 1)', 'VARCHAR(100)') = '%' ) + OR ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') = 'N' + AND co.x.value('substring(@ConstValue, 3, 1)', 'VARCHAR(100)') = '%' ))), + d_nsarg + AS ( SELECT DISTINCT + nsarg.QueryHash + FROM nsarg + WHERE nsarg.fn = 1 + OR nsarg.jo = 1 + OR nsarg.lk = 1 ) +UPDATE b +SET b.is_nonsargable = 1 +FROM d_nsarg AS d +JOIN ##BlitzCacheProcs AS b + ON b.QueryHash = d.QueryHash +WHERE b.SPID = @@SPID +OPTION ( RECOMPILE ); + +/*Begin implicit conversion and parameter info */ + +RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; + +RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT #variable_info ( SPID, QueryHash, SqlHandle, proc_name, variable_name, variable_datatype, compile_time_value ) +SELECT DISTINCT @@SPID, + qp.QueryHash, + qp.SqlHandle, + b.QueryType AS proc_name, + q.n.value('@Column', 'NVARCHAR(258)') AS variable_name, + q.n.value('@ParameterDataType', 'NVARCHAR(258)') AS variable_datatype, + q.n.value('@ParameterCompiledValue', 'NVARCHAR(258)') AS compile_time_value +FROM #query_plan AS qp +JOIN ##BlitzCacheProcs AS b +ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) +OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) +CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) +WHERE b.SPID = @@SPID +OPTION (RECOMPILE); + + +RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +INSERT #conversion_info ( SPID, QueryHash, SqlHandle, proc_name, expression ) +SELECT DISTINCT @@SPID, + qp.QueryHash, + qp.SqlHandle, + b.QueryType AS proc_name, + qq.c.value('@Expression', 'NVARCHAR(4000)') AS expression +FROM #query_plan AS qp +JOIN ##BlitzCacheProcs AS b +ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) +OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) +CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) +WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 + AND qp.QueryHash IS NOT NULL + AND b.implicit_conversions = 1 +AND b.SPID = @@SPID +OPTION (RECOMPILE); + + +RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; +INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) +SELECT @@SPID AS SPID, + ci.SqlHandle, + ci.QueryHash, + REPLACE(REPLACE(REPLACE(ci.proc_name, ')', ''), 'Statement (parent ', ''), 'Procedure or Function: ', '') AS proc_name, + CASE WHEN ci.at_charindex > 0 + AND ci.bracket_charindex > 0 + THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) + ELSE N'**no_variable**' + END AS variable_name, + N'**no_variable**' AS variable_datatype, + CASE WHEN ci.at_charindex = 0 + AND ci.comma_charindex > 0 + AND ci.second_comma_charindex > 0 + THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) + ELSE N'**no_column**' + END AS converted_column_name, + CASE WHEN ci.at_charindex = 0 + AND ci.equal_charindex > 0 + AND ci.convert_implicit_charindex = 0 + THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) + WHEN ci.at_charindex = 0 + AND (ci.equal_charindex -1) > 0 + AND ci.convert_implicit_charindex > 0 + THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) + WHEN ci.at_charindex > 0 + AND ci.comma_charindex > 0 + AND ci.second_comma_charindex > 0 + THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) + ELSE N'**no_column **' + END AS column_name, + CASE WHEN ci.paren_charindex > 0 + AND ci.comma_paren_charindex > 0 + THEN SUBSTRING(ci.expression, ci.paren_charindex, ci.comma_paren_charindex) + END AS converted_to, + 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, 258) AS compile_time_value +FROM #conversion_info AS ci +OPTION (RECOMPILE); + + +RAISERROR(N'Updating variables for inserted procs', 0, 1) WITH NOWAIT; +UPDATE sp +SET sp.variable_datatype = vi.variable_datatype, + sp.compile_time_value = vi.compile_time_value +FROM #stored_proc_info AS sp +JOIN #variable_info AS vi +ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) +OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) +AND sp.variable_name = vi.variable_name +OPTION (RECOMPILE); + + +RAISERROR(N'Inserting variables for other procs', 0, 1) WITH NOWAIT; +INSERT #stored_proc_info + ( SPID, SqlHandle, QueryHash, variable_name, variable_datatype, compile_time_value, proc_name ) +SELECT vi.SPID, vi.SqlHandle, vi.QueryHash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, REPLACE(REPLACE(REPLACE(vi.proc_name, ')', ''), 'Statement (parent ', ''), 'Procedure or Function: ', '') AS proc_name +FROM #variable_info AS vi +WHERE NOT EXISTS +( + SELECT * + FROM #stored_proc_info AS sp + WHERE (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) + OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) +) +OPTION (RECOMPILE); + + +RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; +UPDATE s +SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' + THEN LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) + ELSE s.variable_datatype + END, + s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' + THEN LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) + ELSE s.converted_to + END, + s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' + THEN SUBSTRING(s.compile_time_value, + CHARINDEX('(', s.compile_time_value) + 1, + CHARINDEX(')', s.compile_time_value, CHARINDEX('(', s.compile_time_value) + 1) - 1 - CHARINDEX('(', s.compile_time_value) + ) + WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') + AND s.variable_datatype NOT LIKE '%binary%' + AND s.compile_time_value NOT LIKE 'N''%''' + AND s.compile_time_value NOT LIKE '''%''' + AND s.compile_time_value <> s.column_name + AND s.compile_time_value <> '**idk_man**' + THEN QUOTENAME(compile_time_value, '''') + ELSE s.compile_time_value + END +FROM #stored_proc_info AS s +OPTION (RECOMPILE); + + +RAISERROR(N'Updating SET options', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE s +SET set_options = set_options.ansi_set_options +FROM #stored_proc_info AS s +JOIN ( + SELECT x.SqlHandle, + N'SET ANSI_NULLS ' + CASE WHEN [ANSI_NULLS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_PADDING ' + CASE WHEN [ANSI_PADDING] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_WARNINGS ' + CASE WHEN [ANSI_WARNINGS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ARITHABORT ' + CASE WHEN [ARITHABORT] = 'true' THEN N'ON ' ELSE N' OFF ' END + NCHAR(10) + + N'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN [CONCAT_NULL_YIELDS_NULL] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET NUMERIC_ROUNDABORT ' + CASE WHEN [NUMERIC_ROUNDABORT] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET QUOTED_IDENTIFIER ' + CASE WHEN [QUOTED_IDENTIFIER] = 'true' THEN N'ON ' ELSE N'OFF ' + NCHAR(10) END AS [ansi_set_options] + FROM ( + SELECT + s.SqlHandle, + so.o.value('@ANSI_NULLS', 'NVARCHAR(20)') AS [ANSI_NULLS], + so.o.value('@ANSI_PADDING', 'NVARCHAR(20)') AS [ANSI_PADDING], + so.o.value('@ANSI_WARNINGS', 'NVARCHAR(20)') AS [ANSI_WARNINGS], + so.o.value('@ARITHABORT', 'NVARCHAR(20)') AS [ARITHABORT], + so.o.value('@CONCAT_NULL_YIELDS_NULL', 'NVARCHAR(20)') AS [CONCAT_NULL_YIELDS_NULL], + so.o.value('@NUMERIC_ROUNDABORT', 'NVARCHAR(20)') AS [NUMERIC_ROUNDABORT], + so.o.value('@QUOTED_IDENTIFIER', 'NVARCHAR(20)') AS [QUOTED_IDENTIFIER] + FROM #statements AS s + CROSS APPLY s.statement.nodes('//p:StatementSetOptions') AS so(o) + ) AS x +) AS set_options ON set_options.SqlHandle = s.SqlHandle +OPTION(RECOMPILE); + + +RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + CASE WHEN spi.proc_name <> 'Statement' + THEN N'The stored procedure ' + spi.proc_name + ELSE N'This ad hoc statement' + END + + N' had the following implicit conversions: ' + + CHAR(10) + + STUFF(( + SELECT DISTINCT + @nl + + CASE WHEN spi2.variable_name <> N'**no_variable**' + THEN N'The variable ' + WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN N'The compiled value ' + WHEN spi2.column_name LIKE '%Expr%' + THEN 'The expression ' + ELSE N'The column ' + END + + CASE WHEN spi2.variable_name <> N'**no_variable**' + THEN spi2.variable_name + WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN spi2.compile_time_value + ELSE spi2.column_name + END + + N' has a data type of ' + + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to + ELSE spi2.variable_datatype + END + + N' which caused implicit conversion on the column ' + + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' + THEN spi2.converted_column_name + WHEN spi2.column_name = N'**no_column**' + THEN spi2.converted_column_name + WHEN spi2.converted_column_name = N'**no_column**' + THEN spi2.column_name + WHEN spi2.column_name <> spi2.converted_column_name + THEN spi2.converted_column_name + ELSE spi2.column_name + END + + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') + THEN N'' + WHEN spi2.column_name LIKE '%Expr%' + THEN N'' + WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') + AND spi2.compile_time_value <> spi2.column_name + THEN ' with the value ' + RTRIM(spi2.compile_time_value) + ELSE N'' + END + + '.' + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS implicit_conversion_info +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name +) +UPDATE b +SET b.implicit_conversion_info = pk.implicit_conversion_info +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +OPTION (RECOMPILE); + + +RAISERROR(N'Updating cached parameter XML for stored procs', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + set_options + + @nl + + @nl + + N'EXEC ' + + spi.proc_name + + N' ' + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN @nl + N'More info on possible reasons: https://www.brentozar.com/go/noplans ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + AND spi2.proc_name <> N'Statement' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options +) +UPDATE b +SET b.cached_execution_parameters = pk.cached_execution_parameters +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +WHERE b.QueryType <> N'Statement' +OPTION (RECOMPILE); + + +RAISERROR(N'Updating cached parameter XML for statements', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + set_options + + @nl + + @nl + + N' See QueryText column for full query text' + + @nl + + @nl + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN @nl + N' More info on possible reasons: https://www.brentozar.com/go/noplans ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + AND spi2.proc_name = N'Statement' + AND spi2.variable_name NOT LIKE N'%msparam%' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options +) +UPDATE b +SET b.cached_execution_parameters = pk.cached_execution_parameters +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +WHERE b.QueryType = N'Statement' +OPTION (RECOMPILE); + +RAISERROR(N'Filling in implicit conversion and cached plan parameter info', 0, 1) WITH NOWAIT; +UPDATE b +SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL + OR CONVERT(NVARCHAR(MAX), b.implicit_conversion_info) = N'' + THEN '' + ELSE b.implicit_conversion_info END, + b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL + OR CONVERT(NVARCHAR(MAX), b.cached_execution_parameters) = N'' + THEN '' + ELSE b.cached_execution_parameters END +FROM ##BlitzCacheProcs AS b +WHERE b.SPID = @@SPID +OPTION (RECOMPILE); + +/*End implicit conversion and parameter info*/ + +/*Begin Missing Index*/ +IF EXISTS ( SELECT 1/0 + FROM ##BlitzCacheProcs AS bbcp + WHERE bbcp.missing_index_count > 0 + OR bbcp.index_spool_cost > 0 + OR bbcp.index_spool_rows > 0 + AND bbcp.SPID = @@SPID ) + + BEGIN + RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_xml + SELECT qp.QueryHash, + qp.SqlHandle, + c.mg.value('@Impact', 'FLOAT') AS Impact, + c.mg.query('.') AS cmg + FROM #query_plan AS qp + CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) + WHERE qp.QueryHash IS NOT NULL + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_schema + SELECT mix.QueryHash, mix.SqlHandle, mix.impact, + c.mi.value('@Database', 'NVARCHAR(128)'), + c.mi.value('@Schema', 'NVARCHAR(128)'), + c.mi.value('@Table', 'NVARCHAR(128)'), + c.mi.query('.') + FROM #missing_index_xml AS mix + CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_usage + SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, + c.cg.value('@Usage', 'NVARCHAR(128)'), + c.cg.query('.') + FROM #missing_index_schema ms + CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) + OPTION(RECOMPILE); + + RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) + INSERT #missing_index_detail + SELECT miu.QueryHash, + miu.SqlHandle, + miu.impact, + miu.database_name, + miu.schema_name, + miu.table_name, + miu.usage, + c.c.value('@Name', 'NVARCHAR(128)') + FROM #missing_index_usage AS miu + CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) + OPTION (RECOMPILE); + + RAISERROR(N'Inserting to missing indexes to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + ( QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool ) + SELECT DISTINCT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'EQUALITY' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'INEQUALITY' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name + FROM #missing_index_detail AS m2 + WHERE m2.usage = 'INCLUDE' + AND m.QueryHash = m2.QueryHash + AND m.SqlHandle = m2.SqlHandle + AND m.impact = m2.impact + AND m.database_name = m2.database_name + AND m.schema_name = m2.schema_name + AND m.table_name = m2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS [include], + bbcp.ExecutionCount, + bbcp.QueryPlanCost, + bbcp.PlanCreationTimeHours, + 0 as is_spool + FROM #missing_index_detail AS m + JOIN ##BlitzCacheProcs AS bbcp + ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + OPTION (RECOMPILE); + + RAISERROR(N'Inserting to #index_spool_ugly', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + INSERT #index_spool_ugly + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours) + SELECT p.QueryHash, + p.SqlHandle, + (c.n.value('@EstimateIO', 'FLOAT') + (c.n.value('@EstimateCPU', 'FLOAT'))) + / ( 1 * NULLIF(p.QueryPlanCost, 0)) * 100 AS impact, + o.n.value('@Database', 'NVARCHAR(128)') AS output_database, + o.n.value('@Schema', 'NVARCHAR(128)') AS output_schema, + o.n.value('@Table', 'NVARCHAR(128)') AS output_table, + k.n.value('@Column', 'NVARCHAR(128)') AS range_column, + e.n.value('@Column', 'NVARCHAR(128)') AS expression_column, + o.n.value('@Column', 'NVARCHAR(128)') AS output_column, + p.ExecutionCount, + p.QueryPlanCost, + p.PlanCreationTimeHours + FROM #relop AS r + JOIN ##BlitzCacheProcs p + ON p.QueryHash = r.QueryHash + CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) + CROSS APPLY r.relop.nodes('/p:RelOp/p:OutputList/p:ColumnReference') AS o(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeColumns/p:ColumnReference') AS k(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeExpressions/p:ColumnReference') AS e(n) + WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 + + RAISERROR(N'Inserting to spools to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool) + SELECT DISTINCT + isu.QueryHash, + isu.SqlHandle, + isu.impact, + isu.database_name, + isu.schema_name, + isu.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.equality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.equality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.inequality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.inequality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.include, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.include IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS include, + isu.executions, + isu.query_cost, + isu.creation_hours, + 1 AS is_spool + FROM #index_spool_ugly AS isu + + + RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; + WITH missing AS ( + SELECT DISTINCT + mip.QueryHash, + mip.SqlHandle, + mip.executions, + N'' + AS full_details + FROM #missing_index_pretty AS mip + ) + UPDATE bbcp + SET bbcp.missing_indexes = m.full_details + FROM ##BlitzCacheProcs AS bbcp + JOIN missing AS m + ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + AND m.executions = bbcp.ExecutionCount + AND SPID = @@SPID + OPTION (RECOMPILE); + + END; + + RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; + UPDATE b + SET b.missing_indexes = + CASE WHEN b.missing_indexes IS NULL + THEN '' + ELSE b.missing_indexes + END + FROM ##BlitzCacheProcs AS b + WHERE b.SPID = @@SPID + OPTION (RECOMPILE); + +/*End Missing Index*/ + + +/* Set configuration values */ +RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; +DECLARE @execution_threshold INT = 1000 , + @parameter_sniffing_warning_pct TINYINT = 30, + /* This is in average reads */ + @parameter_sniffing_io_threshold BIGINT = 100000 , + @ctp_threshold_pct TINYINT = 10, + @long_running_query_warning_seconds BIGINT = 300 * 1000 , + @memory_grant_warning_percent INT = 10; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'frequent execution threshold' = LOWER(parameter_name)) +BEGIN + SELECT @execution_threshold = CAST(value AS INT) + FROM #configuration + WHERE 'frequent execution threshold' = LOWER(parameter_name) ; + + SET @msg = ' Setting "frequent execution threshold" to ' + CAST(@execution_threshold AS VARCHAR(10)) ; + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing variance percent' = LOWER(parameter_name)) +BEGIN + SELECT @parameter_sniffing_warning_pct = CAST(value AS TINYINT) + FROM #configuration + WHERE 'parameter sniffing variance percent' = LOWER(parameter_name) ; + + SET @msg = ' Setting "parameter sniffing variance percent" to ' + CAST(@parameter_sniffing_warning_pct AS VARCHAR(3)) ; + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing io threshold' = LOWER(parameter_name)) +BEGIN + SELECT @parameter_sniffing_io_threshold = CAST(value AS BIGINT) + FROM #configuration + WHERE 'parameter sniffing io threshold' = LOWER(parameter_name) ; + + SET @msg = ' Setting "parameter sniffing io threshold" to ' + CAST(@parameter_sniffing_io_threshold AS VARCHAR(10)); + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name)) +BEGIN + SELECT @ctp_threshold_pct = CAST(value AS TINYINT) + FROM #configuration + WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name) ; + + SET @msg = ' Setting "cost threshold for parallelism warning" to ' + CAST(@ctp_threshold_pct AS VARCHAR(3)); + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'long running query warning (seconds)' = LOWER(parameter_name)) +BEGIN + SELECT @long_running_query_warning_seconds = CAST(value * 1000 AS BIGINT) + FROM #configuration + WHERE 'long running query warning (seconds)' = LOWER(parameter_name) ; + + SET @msg = ' Setting "long running query warning (seconds)" to ' + CAST(@long_running_query_warning_seconds AS VARCHAR(10)); + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'unused memory grant' = LOWER(parameter_name)) +BEGIN + SELECT @memory_grant_warning_percent = CAST(value AS INT) + FROM #configuration + WHERE 'unused memory grant' = LOWER(parameter_name) ; + + SET @msg = ' Setting "unused memory grant" to ' + CAST(@memory_grant_warning_percent AS VARCHAR(10)); + + RAISERROR(@msg, 0, 1) WITH NOWAIT; +END; + +DECLARE @ctp INT ; + +SELECT @ctp = NULLIF(CAST(value AS INT), 0) +FROM sys.configurations +WHERE name = 'cost threshold for parallelism' +OPTION (RECOMPILE); + + +/* Update to populate checks columns */ +RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; + +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , + parameter_sniffing = CASE WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold + AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , + near_parallel = CASE WHEN is_parallel <> 1 AND QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, + long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 + WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 + WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, + is_key_lookup_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, + is_sort_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, + is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, + is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, + long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 AND AverageCPU < 500. THEN 1 END, + low_cost_high_cpu = CASE WHEN QueryPlanCost <= 10 AND AverageCPU > 5000. THEN 1 END, + is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, + is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, + is_table_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND table_spool_cost >= QueryPlanCost / 4 THEN 1 END, + is_table_spool_more_rows = CASE WHEN table_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, + is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 1000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 1000) THEN 1 END, + is_big_spills = CASE WHEN (AvgSpills / 128.) > 499. THEN 1 END +WHERE SPID = @@SPID +OPTION (RECOMPILE); + + + +RAISERROR('Checking for forced parameterization and cursors.', 0, 1) WITH NOWAIT; + +/* Set options checks */ +UPDATE p + SET is_forced_parameterized = CASE WHEN (CAST(pa.value AS INT) & 131072 = 131072) THEN 1 END , + is_forced_plan = CASE WHEN (CAST(pa.value AS INT) & 4 = 4) THEN 1 END , + SetOptions = SUBSTRING( + CASE WHEN (CAST(pa.value AS INT) & 1 = 1) THEN ', ANSI_PADDING' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 8 = 8) THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 16 = 16) THEN ', ANSI_WARNINGS' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 32 = 32) THEN ', ANSI_NULLS' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 64 = 64) THEN ', QUOTED_IDENTIFIER' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + + CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END + , 2, 200000) +FROM ##BlitzCacheProcs p + CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa +WHERE pa.attribute = 'set_options' +AND SPID = @@SPID +OPTION (RECOMPILE); + + +/* Cursor checks */ +UPDATE p +SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END +FROM ##BlitzCacheProcs p + CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa +WHERE pa.attribute LIKE '%cursor%' +AND SPID = @@SPID +OPTION (RECOMPILE); + +UPDATE p +SET is_cursor = 1 +FROM ##BlitzCacheProcs p +WHERE QueryHash = 0x0000000000000000 +OR QueryPlanHash = 0x0000000000000000 +AND SPID = @@SPID +OPTION (RECOMPILE); + + + +RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; +/* Populate warnings */ +UPDATE ##BlitzCacheProcs +SET Warnings = SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + + CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + + CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + + CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + + CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + + CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + + CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + + CASE WHEN is_cursor = 1 THEN ', Cursor' + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END + ELSE '' END + + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + + CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + + CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + + CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + + CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + + CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR Function(s)' ELSE '' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + + CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + + CASE WHEN is_table_scan = 1 THEN ', Table Scans (Heaps)' ELSE '' END + + CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + + CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + + CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + + CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + + CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + + CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + + CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + + CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + + CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + + CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + + CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + + CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + + CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + + CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + + CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + + CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + + CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb Spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) +WHERE SPID = @@SPID +OPTION (RECOMPILE); + + +RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; +WITH statement_warnings AS + ( +SELECT DISTINCT + SqlHandle, + Warnings = SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + + CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + + CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + + CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + + CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + + --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + + CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + + CASE WHEN is_cursor = 1 THEN ', Cursor' + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END + ELSE '' END + + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + + CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + + CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + + CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + + CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + + CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' CLR Function(s)' ELSE '' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + + CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + + CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + + CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + + CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + + CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + + CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + + CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + + CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + + CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + + CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + + CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + + CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + + CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + + CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + + CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + + CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + + CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + + CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + + CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) +FROM ##BlitzCacheProcs b +WHERE SPID = @@SPID +AND QueryType LIKE 'Statement (parent%' + ) +UPDATE b +SET b.Warnings = s.Warnings +FROM ##BlitzCacheProcs AS b +JOIN statement_warnings s +ON b.SqlHandle = s.SqlHandle +WHERE QueryType LIKE 'Procedure or Function%' +AND SPID = @@SPID +OPTION (RECOMPILE); + +RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; +WITH plan_handle AS ( +SELECT b.PlanHandle +FROM ##BlitzCacheProcs b + CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp + CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp + WHERE tqp.encrypted = 0 + AND b.SPID = @@SPID + AND (qp.query_plan IS NULL + AND tqp.query_plan IS NOT NULL) +) +UPDATE b +SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' + , 'We couldn''t find a plan for this query. More info on possible reasons: https://www.brentozar.com/go/noplans') +FROM ##BlitzCacheProcs b +LEFT JOIN plan_handle ph ON +b.PlanHandle = ph.PlanHandle +WHERE b.QueryPlan IS NULL +AND b.SPID = @@SPID +OPTION (RECOMPILE); + +RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; +UPDATE ##BlitzCacheProcs +SET Warnings = 'No warnings detected. ' + CASE @ExpertMode + WHEN 0 + THEN ' Try running sp_BlitzCache with @ExpertMode = 1 to find more advanced problems.' + ELSE '' + END +WHERE Warnings = '' OR Warnings IS NULL +AND SPID = @@SPID +OPTION (RECOMPILE); + + +Results: +IF @ExportToExcel = 1 +BEGIN + RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; + + /* excel output */ + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) + OPTION(RECOMPILE); + + SET @sql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT TOP (@Top) + DatabaseName AS [Database Name], + QueryPlanCost AS [Cost], + QueryText, + QueryType AS [Query Type], + Warnings, + ExecutionCount, + ExecutionsPerMinute AS [Executions / Minute], + PercentExecutions AS [Execution Weight], + PercentExecutionsByType AS [% Executions (Type)], + SerialDesiredMemory AS [Serial Desired Memory], + SerialRequiredMemory AS [Serial Required Memory], + TotalCPU AS [Total CPU (ms)], + AverageCPU AS [Avg CPU (ms)], + PercentCPU AS [CPU Weight], + PercentCPUByType AS [% CPU (Type)], + TotalDuration AS [Total Duration (ms)], + AverageDuration AS [Avg Duration (ms)], + PercentDuration AS [Duration Weight], + PercentDurationByType AS [% Duration (Type)], + TotalReads AS [Total Reads], + AverageReads AS [Average Reads], + PercentReads AS [Read Weight], + PercentReadsByType AS [% Reads (Type)], + TotalWrites AS [Total Writes], + AverageWrites AS [Average Writes], + PercentWrites AS [Write Weight], + PercentWritesByType AS [% Writes (Type)], + TotalReturnedRows, + AverageReturnedRows, + MinReturnedRows, + MaxReturnedRows, + MinGrantKB, + MaxGrantKB, + MinUsedGrantKB, + MaxUsedGrantKB, + PercentMemoryGrantUsed, + AvgMaxMemoryGrant, + MinSpills, + MaxSpills, + TotalSpills, + AvgSpills, + NumberOfPlans, + NumberOfDistinctPlans, + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + StatementStartOffset, + StatementEndOffset, + PlanGenerationNum, + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + QueryHash, + QueryPlanHash, + COALESCE(SetOptions, '''') AS [SET Options] + FROM ##BlitzCacheProcs + WHERE 1 = 1 + AND SPID = @@SPID ' + @nl; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + + IF @MinutesBack IS NOT NULL + BEGIN + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + + SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'duplicate' THEN N' plan_multiple_plans ' /* Issue #3345 */ + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + END + N' DESC '; + + SET @sql += N' OPTION (RECOMPILE) ; '; + + IF @sql IS NULL + BEGIN + RAISERROR('@sql is null, which means dynamic SQL generation went terribly wrong', 0, 1) WITH NOWAIT; + END + + IF @Debug = 1 + BEGIN + RAISERROR('Printing @sql, the dynamic SQL we generated:', 0, 1) WITH NOWAIT; + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; + + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; +END; + + +RAISERROR('Displaying analysis of plan cache.', 0, 1) WITH NOWAIT; + +DECLARE @columns NVARCHAR(MAX) = N'' ; + +IF @ExpertMode = 0 +BEGIN + RAISERROR(N'Returning ExpertMode = 0', 0, 1) WITH NOWAIT; + SET @columns = N' DatabaseName AS [Database], + QueryPlanCost AS [Cost], + QueryText AS [Query Text], + QueryType AS [Query Type], + Warnings AS [Warnings], + QueryPlan AS [Query Plan], + missing_indexes AS [Missing Indexes], + implicit_conversion_info AS [Implicit Conversion Info], + cached_execution_parameters AS [Cached Execution Parameters], + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Avg Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Avg Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Average Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + LastCompletionTime AS [Last Completion], + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + COALESCE(SetOptions, '''') AS [SET Options], + QueryHash AS [Query Hash], + PlanGenerationNum, + [Remove Plan Handle From Cache]'; +END; +ELSE +BEGIN + SET @columns = N' DatabaseName AS [Database], + QueryPlanCost AS [Cost], + QueryText AS [Query Text], + QueryType AS [Query Type], + Warnings AS [Warnings], + QueryPlan AS [Query Plan], + missing_indexes AS [Missing Indexes], + implicit_conversion_info AS [Implicit Conversion Info], + cached_execution_parameters AS [Cached Execution Parameters], ' + @nl; + + IF @ExpertMode = 2 /* Opserver */ + BEGIN + RAISERROR(N'Returning Expert Mode = 2', 0, 1) WITH NOWAIT; + SET @columns += N' + SUBSTRING( + CASE WHEN warning_no_join_predicate = 1 THEN '', 20'' ELSE '''' END + + CASE WHEN compile_timeout = 1 THEN '', 18'' ELSE '''' END + + CASE WHEN compile_memory_limit_exceeded = 1 THEN '', 19'' ELSE '''' END + + CASE WHEN busy_loops = 1 THEN '', 16'' ELSE '''' END + + CASE WHEN is_forced_plan = 1 THEN '', 3'' ELSE '''' END + + CASE WHEN is_forced_parameterized > 0 THEN '', 5'' ELSE '''' END + + CASE WHEN unparameterized_query = 1 THEN '', 23'' ELSE '''' END + + CASE WHEN missing_index_count > 0 THEN '', 10'' ELSE '''' END + + CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + + CASE WHEN is_cursor = 1 THEN '', 4'' ELSE '''' END + + CASE WHEN is_parallel = 1 THEN '', 6'' ELSE '''' END + + CASE WHEN near_parallel = 1 THEN '', 7'' ELSE '''' END + + CASE WHEN frequent_execution = 1 THEN '', 1'' ELSE '''' END + + CASE WHEN plan_warnings = 1 THEN '', 8'' ELSE '''' END + + CASE WHEN parameter_sniffing = 1 THEN '', 2'' ELSE '''' END + + CASE WHEN long_running = 1 THEN '', 9'' ELSE '''' END + + CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + + CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + + CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + + CASE WHEN plan_multiple_plans > 0 THEN '', 21'' ELSE '''' END + + CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + + CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + + CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + + CASE WHEN is_key_lookup_expensive = 1 THEN '', 26'' ELSE '''' END + + CASE WHEN is_remote_query_expensive = 1 THEN '', 28'' ELSE '''' END + + CASE WHEN trace_flags_session IS NOT NULL THEN '', 29'' ELSE '''' END + + CASE WHEN is_unused_grant = 1 THEN '', 30'' ELSE '''' END + + CASE WHEN function_count > 0 THEN '', 31'' ELSE '''' END + + CASE WHEN clr_function_count > 0 THEN '', 32'' ELSE '''' END + + CASE WHEN PlanCreationTimeHours <= 4 THEN '', 33'' ELSE '''' END + + CASE WHEN is_table_variable = 1 THEN '', 34'' ELSE '''' END + + CASE WHEN no_stats_warning = 1 THEN '', 35'' ELSE '''' END + + CASE WHEN relop_warnings = 1 THEN '', 36'' ELSE '''' END + + CASE WHEN is_table_scan = 1 THEN '', 37'' ELSE '''' END + + CASE WHEN backwards_scan = 1 THEN '', 38'' ELSE '''' END + + CASE WHEN forced_index = 1 THEN '', 39'' ELSE '''' END + + CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '', 40'' ELSE '''' END + + CASE WHEN columnstore_row_mode = 1 THEN '', 41'' ELSE '''' END + + CASE WHEN is_computed_scalar = 1 THEN '', 42'' ELSE '''' END + + CASE WHEN is_sort_expensive = 1 THEN '', 43'' ELSE '''' END + + CASE WHEN is_computed_filter = 1 THEN '', 44'' ELSE '''' END + + CASE WHEN index_ops >= 5 THEN '', 45'' ELSE '''' END + + CASE WHEN is_row_level = 1 THEN '', 46'' ELSE '''' END + + CASE WHEN is_spatial = 1 THEN '', 47'' ELSE '''' END + + CASE WHEN index_dml = 1 THEN '', 48'' ELSE '''' END + + CASE WHEN table_dml = 1 THEN '', 49'' ELSE '''' END + + CASE WHEN long_running_low_cpu = 1 THEN '', 50'' ELSE '''' END + + CASE WHEN low_cost_high_cpu = 1 THEN '', 51'' ELSE '''' END + + CASE WHEN stale_stats = 1 THEN '', 52'' ELSE '''' END + + CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + + CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + + CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + + CASE WHEN is_table_spool_expensive = 1 THEN + '', 67'' ELSE '''' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + '', 68'' ELSE '''' END + + CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + + CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END + + CASE WHEN is_row_goal = 1 THEN '', 58'' ELSE '''' END + + CASE WHEN is_big_spills = 1 THEN '', 59'' ELSE '''' END + + CASE WHEN is_mstvf = 1 THEN '', 60'' ELSE '''' END + + CASE WHEN is_mm_join = 1 THEN '', 61'' ELSE '''' END + + CASE WHEN is_nonsargable = 1 THEN '', 62'' ELSE '''' END + + CASE WHEN CompileTime > 5000 THEN '', 63 '' ELSE '''' END + + CASE WHEN CompileCPU > 5000 THEN '', 64 '' ELSE '''' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN '', 65 '' ELSE '''' END + + CASE WHEN select_with_writes > 0 THEN '', 66'' ELSE '''' END + , 3, 200000) AS opserver_warning , ' + @nl ; + END; + + SET @columns += N' + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((SerialDesiredMemory) AS BIGINT), 1) AS [Serial Desired Memory], + CONVERT(NVARCHAR(30), CAST((SerialRequiredMemory) AS BIGINT), 1) AS [Serial Required Memory], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Average Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Average Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((PercentExecutionsByType) AS BIGINT), 1) AS [% Executions (Type)], + CONVERT(NVARCHAR(30), CAST((PercentCPUByType) AS BIGINT), 1) AS [% CPU (Type)], + CONVERT(NVARCHAR(30), CAST((PercentDurationByType) AS BIGINT), 1) AS [% Duration (Type)], + CONVERT(NVARCHAR(30), CAST((PercentReadsByType) AS BIGINT), 1) AS [% Reads (Type)], + CONVERT(NVARCHAR(30), CAST((PercentWritesByType) AS BIGINT), 1) AS [% Writes (Type)], + CONVERT(NVARCHAR(30), CAST((TotalReturnedRows) AS BIGINT), 1) AS [Total Rows], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Avg Rows], + CONVERT(NVARCHAR(30), CAST((MinReturnedRows) AS BIGINT), 1) AS [Min Rows], + CONVERT(NVARCHAR(30), CAST((MaxReturnedRows) AS BIGINT), 1) AS [Max Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], + CONVERT(NVARCHAR(30), CAST((NumberOfPlans) AS BIGINT), 1) AS [# Plans], + CONVERT(NVARCHAR(30), CAST((NumberOfDistinctPlans) AS BIGINT), 1) AS [# Distinct Plans], + PlanCreationTime AS [Created At], + LastExecutionTime AS [Last Execution], + LastCompletionTime AS [Last Completion], + CONVERT(NVARCHAR(30), CAST((CachedPlanSize) AS BIGINT), 1) AS [Cached Plan Size (KB)], + CONVERT(NVARCHAR(30), CAST((CompileTime) AS BIGINT), 1) AS [Compile Time (ms)], + CONVERT(NVARCHAR(30), CAST((CompileCPU) AS BIGINT), 1) AS [Compile CPU (ms)], + CONVERT(NVARCHAR(30), CAST((CompileMemory) AS BIGINT), 1) AS [Compile memory (KB)], + COALESCE(SetOptions, '''') AS [SET Options], + PlanHandle AS [Plan Handle], + SqlHandle AS [SQL Handle], + [SQL Handle More Info], + QueryHash AS [Query Hash], + [Query Hash More Info], + QueryPlanHash AS [Query Plan Hash], + StatementStartOffset, + StatementEndOffset, + PlanGenerationNum, + [Remove Plan Handle From Cache], + [Remove SQL Handle From Cache]'; +END; + +SET @sql = N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +SELECT TOP (@Top) ' + @columns + @nl + N' +FROM ##BlitzCacheProcs +WHERE SPID = @spid ' + @nl; + +IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount ' + @nl; + END; + +IF @MinutesBack IS NOT NULL + BEGIN + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; + END; + +SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB ' + WHEN N'duplicate' THEN N' plan_multiple_plans ' + WHEN N'spills' THEN N' MaxSpills ' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + END + N' DESC '; +SET @sql += N' OPTION (RECOMPILE) ; '; + +IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; +IF(@OutputType <> 'NONE') +BEGIN + EXEC sp_executesql @sql, N'@Top INT, @spid INT, @MinimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; +END; + +/* + +This section will check if: + * >= 30% of plans were created in the last hour + * Check on the memory_clerks DMV for space used by TokenAndPermUserStore + * Compare that to the size of the buffer pool + * If it's >10%, +*/ +IF EXISTS +( + SELECT 1/0 + FROM #plan_creation AS pc + WHERE pc.percent_1 >= 30 +) +BEGIN + +SELECT @common_version = + CONVERT(DECIMAL(10,2), c.common_version) +FROM #checkversion AS c; + +IF @common_version >= 11 + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' +ELSE + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(single_pages_kb + multi_pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' + +EXEC sys.sp_executesql @user_perm_sql, + N'@buffer_pool_memory_gb DECIMAL(10,2) OUTPUT', + @buffer_pool_memory_gb = @buffer_pool_memory_gb OUTPUT; + +IF @common_version >= 11 +BEGIN + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN (pages_kb / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), (pages_kb / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; +END; + +IF @common_version < 11 +BEGIN + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; +END; + +EXEC sys.sp_executesql @user_perm_sql, + N'@user_perm_gb DECIMAL(10,2) OUTPUT', + @user_perm_gb = @user_perm_gb_out OUTPUT; + +IF @buffer_pool_memory_gb > 0 + BEGIN + IF (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100. >= 10 + BEGIN + SET @is_tokenstore_big = 1; + SET @user_perm_percent = (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100.; + END + END + +END + + + +IF @HideSummary = 0 AND @ExportToExcel = 0 +BEGIN + IF @Reanalyze = 0 + BEGIN + RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; + + /* Build summary data */ + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE frequent_execution = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 1, + 100, + 'Execution Pattern', + 'Frequent Execution', + 'https://www.brentozar.com/blitzcache/frequently-executed-queries/', + 'Queries are being executed more than ' + + CAST (@execution_threshold AS VARCHAR(5)) + + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE parameter_sniffing = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 2, + 50, + 'Parameterization', + 'Parameter Sniffing', + 'https://www.brentozar.com/blitzcache/parameter-sniffing/', + 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; + + /* Forced execution plans */ + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_forced_plan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 3, + 50, + 'Parameterization', + 'Forced Plan', + 'https://www.brentozar.com/blitzcache/forced-plans/', + 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Cursor', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_optimistic_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Optimistic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are optimistic cursors in the plan cache, which can harm performance.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_forward_only_cursor = 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Non-forward Only Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are non-forward only cursors in the plan cache, which can harm performance.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_cursor_dynamic = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Dynamic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Dynamic Cursors inhibit parallelism!.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_fast_forward_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Fast Forward Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Fast forward cursors inhibit parallelism!.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_forced_parameterized = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 5, + 50, + 'Parameterization', + 'Forced Parameterization', + 'https://www.brentozar.com/blitzcache/forced-parameterization/', + 'Execution plans have been compiled with forced parameterization.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_parallel = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 6, + 200, + 'Execution Plans', + 'Parallel', + 'https://www.brentozar.com/blitzcache/parallel-plans-detected/', + 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE near_parallel = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 7, + 200, + 'Execution Plans', + 'Nearly Parallel', + 'https://www.brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', + 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE plan_warnings = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 8, + 50, + 'Execution Plans', + 'Plan Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', + 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE long_running = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 9, + 50, + 'Performance', + 'Long Running Query', + 'https://www.brentozar.com/blitzcache/long-running-queries/', + 'Long running queries have been found. These are queries with an average duration longer than ' + + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) + + ' second(s). These queries should be investigated for additional tuning options.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.missing_index_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 10, + 50, + 'Performance', + 'Missing Indexes', + 'https://www.brentozar.com/blitzcache/missing-index-request/', + 'Queries found with missing indexes.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.downlevel_estimator = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 13, + 200, + 'Cardinality', + 'Downlevel CE', + 'https://www.brentozar.com/blitzcache/legacy-cardinality-estimator/', + 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE implicit_conversions = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 14, + 50, + 'Performance', + 'Implicit Conversions', + 'https://www.brentozar.com/go/implicit', + 'One or more queries are comparing two fields that are not of the same data type.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE busy_loops = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 16, + 100, + 'Performance', + 'Busy Loops', + 'https://www.brentozar.com/blitzcache/busy-loops/', + 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE tvf_join = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 17, + 50, + 'Performance', + 'Function Join', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE compile_timeout = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 18, + 50, + 'Execution Plans', + 'Compilation Timeout', + 'https://www.brentozar.com/blitzcache/compilation-timeout/', + 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE compile_memory_limit_exceeded = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 19, + 50, + 'Execution Plans', + 'Compile Memory Limit Exceeded', + 'https://www.brentozar.com/blitzcache/compile-memory-limit-exceeded/', + 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE warning_no_join_predicate = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 20, + 50, + 'Execution Plans', + 'No Join Predicate', + 'https://www.brentozar.com/blitzcache/no-join-predicate/', + 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE plan_multiple_plans > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 21, + 200, + 'Execution Plans', + 'Multiple Plans', + 'https://www.brentozar.com/blitzcache/multiple-plans/', + 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE unmatched_index_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 22, + 100, + 'Performance', + 'Unmatched Indexes', + 'https://www.brentozar.com/blitzcache/unmatched-indexes', + 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE unparameterized_query = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 23, + 100, + 'Parameterization', + 'Unparameterized Query', + 'https://www.brentozar.com/blitzcache/unparameterized-queries', + 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_trivial = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 24, + 100, + 'Execution Plans', + 'Trivial Plans', + 'https://www.brentozar.com/blitzcache/trivial-plans', + 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_forced_serial= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 25, + 10, + 'Execution Plans', + 'Forced Serialization', + 'https://www.brentozar.com/blitzcache/forced-serialization/', + 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_key_lookup_expensive= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 26, + 100, + 'Execution Plans', + 'Expensive Key Lookup', + 'https://www.brentozar.com/blitzcache/expensive-key-lookups/', + 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_remote_query_expensive= 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 28, + 100, + 'Execution Plans', + 'Expensive Remote Query', + 'https://www.brentozar.com/blitzcache/expensive-remote-query/', + 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.trace_flags_session IS NOT NULL + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 29, + 200, + 'Trace Flags', + 'Session Level Trace Flags Enabled', + 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', + 'Someone is enabling session level Trace Flags in a query.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_unused_grant IS NOT NULL + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 30, + 100, + 'Memory Grant', + 'Unused Memory Grant', + 'https://www.brentozar.com/blitzcache/unused-memory-grants/', + 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.function_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 31, + 100, + 'Compute Scalar That References A Function', + 'Calls Functions', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.clr_function_count > 0 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 32, + 100, + 'Compute Scalar That References A CLR Function', + 'Calls CLR Functions', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'May force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; + + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_variable = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 33, + 100, + 'Table Variables detected', + 'Table Variables', + 'https://www.brentozar.com/blitzcache/table-variables/', + 'All modifications are single threaded, and selects have really low row estimates.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.no_stats_warning = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 35, + 100, + 'Statistics', + 'Columns With No Statistics', + 'https://www.brentozar.com/blitzcache/columns-no-statistics/', + 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.relop_warnings = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 36, + 100, + 'Warnings', + 'Operator Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', + 'Check the plan for more details.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 37, + 100, + 'Indexes', + 'Table Scans (Heaps)', + 'https://www.brentozar.com/archive/2012/05/video-heaps/', + 'This may not be a problem. Run sp_BlitzIndex for more information.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.backwards_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 38, + 200, + 'Indexes', + 'Backwards Scans', + 'https://www.brentozar.com/blitzcache/backwards-scans/', + 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_index = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 39, + 100, + 'Indexes', + 'Forced Indexes', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans, and will prevent missing index requests.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_seek = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 40, + 100, + 'Indexes', + 'Forced Seeks', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 40, + 100, + 'Indexes', + 'Forced Scans', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.columnstore_row_mode = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 41, + 100, + 'Indexes', + 'ColumnStore Row Mode', + 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', + 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_computed_scalar = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 42, + 50, + 'Functions', + 'Computed Column UDF', + 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', + 'This can cause a whole mess of bad serializartion problems.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_sort_expensive = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 43, + 100, + 'Execution Plans', + 'Expensive Sort', + 'https://www.brentozar.com/blitzcache/expensive-sorts/', + 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_computed_filter = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 44, + 50, + 'Functions', + 'Filter UDF', + 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', + 'Someone put a Scalar UDF in the WHERE clause!') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.index_ops >= 5 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 45, + 100, + 'Indexes', + '>= 5 Indexes Modified', + 'https://www.brentozar.com/blitzcache/many-indexes-modified/', + 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_row_level = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 46, + 200, + 'Complexity', + 'Row Level Security', + 'https://www.brentozar.com/blitzcache/row-level-security/', + 'You may see a lot of confusing junk in your query plan.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_spatial = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 47, + 200, + 'Complexity', + 'Spatial Index', + 'https://www.brentozar.com/blitzcache/spatial-indexes/', + 'Purely informational.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.index_dml = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 48, + 150, + 'Complexity', + 'Index DML', + 'https://www.brentozar.com/blitzcache/index-dml/', + 'This can cause recompiles and stuff.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.table_dml = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 49, + 150, + 'Complexity', + 'Table DML', + 'https://www.brentozar.com/blitzcache/table-dml/', + 'This can cause recompiles and stuff.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.long_running_low_cpu = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 50, + 150, + 'Blocking', + 'Long Running Low CPU', + 'https://www.brentozar.com/blitzcache/long-running-low-cpu/', + 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.low_cost_high_cpu = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 51, + 150, + 'Complexity', + 'Low Cost Query With High CPU', + 'https://www.brentozar.com/blitzcache/low-cost-high-cpu/', + 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.stale_stats = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 52, + 150, + 'Statistics', + 'Statistics used have > 100k modifications in the last 7 days', + 'https://www.brentozar.com/blitzcache/stale-statistics/', + 'Ever heard of updating statistics?') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_adaptive = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 53, + 200, + 'Complexity', + 'Adaptive joins', + 'https://www.brentozar.com/blitzcache/adaptive-joins/', + 'This join will sometimes do seeks, and sometimes do scans.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_spool_expensive = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 54, + 150, + 'Indexes', + 'Expensive Index Spool', + 'https://www.brentozar.com/blitzcache/eager-index-spools/', + 'Check operator predicates and output for index definition guidance') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_spool_more_rows = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 55, + 150, + 'Indexes', + 'Large Index Row Spool', + 'https://www.brentozar.com/blitzcache/eager-index-spools/', + 'Check operator predicates and output for index definition guidance') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_bad_estimate = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 56, + 100, + 'Complexity', + 'Row Estimate Mismatch', + 'https://www.brentozar.com/blitzcache/bad-estimates/', + 'Estimated rows are different from average rows by a factor of 10000. This may indicate a performance problem if mismatches occur regularly') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_paul_white_electric = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 57, + 200, + 'Is Paul White Electric?', + 'This query has a Switch operator in it!', + 'https://www.sql.kiwi/2013/06/hello-operator-my-switch-is-bored.html', + 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; + + IF @v >= 14 OR (@v = 13 AND @build >= 5026) + BEGIN + + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + @@SPID, + 997, + 200, + 'Database Level Statistics', + 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], + 'https://www.brentozar.com/blitzcache/stale-statistics/' AS URL, + 'Consider updating statistics more frequently,' AS [Details] + FROM #stats_agg AS sa + GROUP BY sa.[Database] + HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) + AND AVG(sa.ModificationCount) >= 100000; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_row_goal = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 58, + 200, + 'Complexity', + 'Row Goals', + 'https://www.brentozar.com/go/rowgoals/', + 'This query had row goals introduced, which can be good or bad, and should be investigated for high read queries.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_big_spills = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 59, + 100, + 'TempDB', + '>500mb Spills', + 'https://www.brentozar.com/blitzcache/tempdb-spills/', + 'This query spills >500mb to tempdb on average. One way or another, this query didn''t get enough memory') ; + + + END; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_mstvf = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 60, + 100, + 'Functions', + 'MSTVFs', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_mm_join = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 61, + 100, + 'Complexity', + 'Many to Many Merge', + 'https://www.brentozar.com/archive/2018/04/many-mysteries-merge-joins/', + 'These use secret worktables that could be doing lots of reads. Occurs when join inputs aren''t known to be unique. Can be really bad when parallel.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_nonsargable = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 62, + 50, + 'Non-SARGable queries', + 'non-SARGables', + 'https://www.brentozar.com/blitzcache/non-sargable-predicates/', + 'Looks for intrinsic functions and expressions as predicates, and leading wildcard LIKE searches.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileTime > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 63, + 100, + 'Complexity', + 'Long Compile Time', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries are taking >5 seconds to compile. This can be normal for large plans, but be careful if they compile frequently'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileCPU > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 64, + 50, + 'Complexity', + 'High Compile CPU', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking >5 seconds of CPU to compile. If CPU is high and plans like this compile frequently, they may be related'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileMemory > 1024 + AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 65, + 50, + 'Complexity', + 'High Compile Memory', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking 10% of Max Compile Memory. If you see high RESOURCE_SEMAPHORE_QUERY_COMPILE waits, these may be related'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.select_with_writes = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 66, + 50, + 'Complexity', + 'Selects w/ Writes', + 'https://dba.stackexchange.com/questions/191825/', + 'This is thrown when reads cause writes that are not already flagged as big spills (2016+) or index spools.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_expensive = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 67, + 150, + 'Expensive Table Spool', + 'You have a table spool, this is usually a sign that queries are doing unnecessary work', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_more_rows = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 68, + 150, + 'Table Spools Many Rows', + 'You have a table spool that spools more rows than the query returns', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join'); + + IF EXISTS (SELECT 1/0 + FROM #plan_creation p + WHERE (p.percent_24 > 0) + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT SPID, + 999, + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 'Plan Cache Instability' ELSE 'Plan Cache Stability' END AS Finding, + 'https://www.brentozar.com/archive/2018/07/tsql2sday-how-much-plan-cache-history-do-you-have/', + 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) + + ' total plans in your cache, with ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) + + '% plans created in the past 24 hours, ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) + + '% created in the past 4 hours, and ' + + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) + + '% created in the past 1 hour. ' + + 'When these percentages are high, it may be a sign of memory pressure or plan cache instability.' + FROM #plan_creation p ; + + IF EXISTS (SELECT 1/0 + FROM #plan_usage p + WHERE p.percent_duplicate > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 'Many Duplicate Plans' ELSE 'Duplicate Plans' END AS Finding, + 'https://www.brentozar.com/archive/2018/03/why-multiple-plans-for-one-query-are-bad/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_duplicate) + + '% are duplicates with more than 5 entries' + + ', meaning similar queries are generating the same plan repeatedly.' + + ' Forced Parameterization may fix the issue. To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; + + IF EXISTS (SELECT 1/0 + FROM #plan_usage p + WHERE p.percent_single > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 'Many Single-Use Plans' ELSE 'Single-Use Plans' END AS Finding, + 'https://www.brentozar.com/blitz/single-use-plans-procedure-cache/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_single) + + '% are single use plans' + + ', meaning SQL Server thinks it''s seeing a lot of "new" queries and creating plans for them.' + + ' Forced Parameterization and/or Optimize For Ad Hoc Workloads may fix the issue.' + + 'To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; + + IF @is_tokenstore_big = 1 + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT @@SPID, + 69, + 10, + N'Large USERSTORE_TOKENPERM cache: ' + CONVERT(NVARCHAR(11), @user_perm_gb_out) + N'GB', + N'The USERSTORE_TOKENPERM is taking up ' + CONVERT(NVARCHAR(11), @user_perm_percent) + + N'% of the buffer pool, and your plan cache seems to be unstable', + N'https://www.brentozar.com/go/userstore', + N'A growing USERSTORE_TOKENPERM cache can cause the plan cache to clear out' + + IF @v >= 11 + BEGIN + IF EXISTS (SELECT 1/0 + FROM #trace_flags AS tf + WHERE tf.global_trace_flags IS NOT NULL + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 1000, + 255, + 'Global Trace Flags Enabled', + 'You have Global Trace Flags enabled on your server', + 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', + 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; + END; + + IF NOT EXISTS (SELECT 1/0 + FROM ##BlitzCacheResults AS bcr + WHERE bcr.Priority = 2147483646 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 2147483646, + 255, + 'Need more help?' , + 'Paste your plan on the internet!', + 'http://pastetheplan.com', + 'This makes it easy to share plans and post them to Q&A sites like https://dba.stackexchange.com/!') ; + + + + IF NOT EXISTS (SELECT 1/0 + FROM ##BlitzCacheResults AS bcr + WHERE bcr.Priority = 2147483647 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 2147483647, + 255, + 'Thanks for using sp_BlitzCache!' , + 'From Your Community Volunteers', + 'http://FirstResponderKit.org', + 'We hope you found this tool useful. Current version: ' + @Version + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.') ; + + END; + + + SELECT Priority, + FindingsGroup, + Finding, + URL, + Details, + CheckID + FROM ##BlitzCacheResults + WHERE SPID = @@SPID + GROUP BY Priority, + FindingsGroup, + Finding, + URL, + Details, + CheckID + ORDER BY Priority ASC, FindingsGroup, Finding, CheckID ASC + OPTION (RECOMPILE); +END; + +IF @Debug = 1 + BEGIN + + SELECT '##BlitzCacheResults' AS table_name, * + FROM ##BlitzCacheResults + OPTION ( RECOMPILE ); + + SELECT '##BlitzCacheProcs' AS table_name, * + FROM ##BlitzCacheProcs + OPTION ( RECOMPILE ); + + SELECT '#statements' AS table_name, * + FROM #statements AS s + OPTION (RECOMPILE); + + SELECT '#query_plan' AS table_name, * + FROM #query_plan AS qp + OPTION (RECOMPILE); + + SELECT '#relop' AS table_name, * + FROM #relop AS r + OPTION (RECOMPILE); + + SELECT '#only_query_hashes' AS table_name, * + FROM #only_query_hashes + OPTION ( RECOMPILE ); + + SELECT '#ignore_query_hashes' AS table_name, * + FROM #ignore_query_hashes + OPTION ( RECOMPILE ); + + SELECT '#only_sql_handles' AS table_name, * + FROM #only_sql_handles + OPTION ( RECOMPILE ); + + SELECT '#ignore_sql_handles' AS table_name, * + FROM #ignore_sql_handles + OPTION ( RECOMPILE ); + + SELECT '#p' AS table_name, * + FROM #p + OPTION ( RECOMPILE ); + + SELECT '#checkversion' AS table_name, * + FROM #checkversion + OPTION ( RECOMPILE ); + + SELECT '#configuration' AS table_name, * + FROM #configuration + OPTION ( RECOMPILE ); + + SELECT '#stored_proc_info' AS table_name, * + FROM #stored_proc_info + OPTION ( RECOMPILE ); + + SELECT '#conversion_info' AS table_name, * + FROM #conversion_info AS ci + OPTION ( RECOMPILE ); + + SELECT '#variable_info' AS table_name, * + FROM #variable_info AS vi + OPTION ( RECOMPILE ); + + SELECT '#missing_index_xml' AS table_name, * + FROM #missing_index_xml AS mix + OPTION ( RECOMPILE ); + + SELECT '#missing_index_schema' AS table_name, * + FROM #missing_index_schema AS mis + OPTION ( RECOMPILE ); + + SELECT '#missing_index_usage' AS table_name, * + FROM #missing_index_usage AS miu + OPTION ( RECOMPILE ); + + SELECT '#missing_index_detail' AS table_name, * + FROM #missing_index_detail AS mid + OPTION ( RECOMPILE ); + + SELECT '#missing_index_pretty' AS table_name, * + FROM #missing_index_pretty AS mip + OPTION ( RECOMPILE ); + + SELECT '#plan_creation' AS table_name, * + FROM #plan_creation + OPTION ( RECOMPILE ); + + SELECT '#plan_cost' AS table_name, * + FROM #plan_cost + OPTION ( RECOMPILE ); + + SELECT '#proc_costs' AS table_name, * + FROM #proc_costs + OPTION ( RECOMPILE ); + + SELECT '#stats_agg' AS table_name, * + FROM #stats_agg + OPTION ( RECOMPILE ); + + SELECT '#trace_flags' AS table_name, * + FROM #trace_flags + OPTION ( RECOMPILE ); + + SELECT '#plan_usage' AS table_name, * + FROM #plan_usage + OPTION ( RECOMPILE ); + + END; + + IF @OutputTableName IS NOT NULL + --Allow for output to ##DB so don't check for DB or schema name here + GOTO OutputResultsToTable; +RETURN; --Avoid going into the AllSort GOTO + +/*Begin code to sort by all*/ +AllSorts: +RAISERROR('Beginning all sort loop', 0, 1) WITH NOWAIT; + + +IF ( + @Top > 10 + AND @SkipAnalysis = 0 + AND @BringThePain = 0 + ) + BEGIN + RAISERROR( + ' + You''ve chosen a value greater than 10 to sort the whole plan cache by. + That can take a long time and harm performance. + Please choose a number <= 10, or set @BringThePain = 1 to signify you understand this might be a bad idea. + ', 0, 1) WITH NOWAIT; + RETURN; + END; + + +IF OBJECT_ID('tempdb..#checkversion_allsort') IS NULL + BEGIN + CREATE TABLE #checkversion_allsort + ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) + ); + + INSERT INTO #checkversion_allsort + (version) + SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + OPTION ( RECOMPILE ); + END; + + +SELECT @v = common_version, + @build = build +FROM #checkversion_allsort +OPTION ( RECOMPILE ); + +IF OBJECT_ID('tempdb.. #bou_allsort') IS NULL + BEGIN + CREATE TABLE #bou_allsort + ( + Id INT IDENTITY(1, 1), + DatabaseName NVARCHAR(128), + Cost FLOAT, + QueryText NVARCHAR(MAX), + QueryType NVARCHAR(258), + Warnings VARCHAR(MAX), + QueryPlan XML, + missing_indexes XML, + implicit_conversion_info XML, + cached_execution_parameters XML, + ExecutionCount NVARCHAR(30), + ExecutionsPerMinute MONEY, + ExecutionWeight MONEY, + TotalCPU NVARCHAR(30), + AverageCPU NVARCHAR(30), + CPUWeight MONEY, + TotalDuration NVARCHAR(30), + AverageDuration NVARCHAR(30), + DurationWeight MONEY, + TotalReads NVARCHAR(30), + AverageReads NVARCHAR(30), + ReadWeight MONEY, + TotalWrites NVARCHAR(30), + AverageWrites NVARCHAR(30), + WriteWeight MONEY, + AverageReturnedRows MONEY, + MinGrantKB NVARCHAR(30), + MaxGrantKB NVARCHAR(30), + MinUsedGrantKB NVARCHAR(30), + MaxUsedGrantKB NVARCHAR(30), + AvgMaxMemoryGrant MONEY, + MinSpills NVARCHAR(30), + MaxSpills NVARCHAR(30), + TotalSpills NVARCHAR(30), + AvgSpills MONEY, + PlanCreationTime DATETIME, + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + SqlHandle VARBINARY(64), + SetOptions VARCHAR(MAX), + QueryHash BINARY(8), + PlanGenerationNum NVARCHAR(30), + RemovePlanHandleFromCache NVARCHAR(200), + Pattern NVARCHAR(20) + ); + END; + + +IF @SortOrder = 'all' +BEGIN +RAISERROR('Beginning for ALL', 0, 1) WITH NOWAIT; +SET @AllSortSql += N' + DECLARE @ISH NVARCHAR(MAX) = N'''' + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + '; + + IF @VersionShowsMemoryGrants = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsMemoryGrants = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; +END; + + +IF @SortOrder = 'all avg' +BEGIN +RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; +SET @AllSortSql += N' + DECLARE @ISH NVARCHAR(MAX) = N'''' + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); + + '; + + IF @VersionShowsMemoryGrants = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsMemoryGrants = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; +END; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@AllSortSql, 0, 4000); + PRINT SUBSTRING(@AllSortSql, 4000, 8000); + PRINT SUBSTRING(@AllSortSql, 8000, 12000); + PRINT SUBSTRING(@AllSortSql, 12000, 16000); + PRINT SUBSTRING(@AllSortSql, 16000, 20000); + PRINT SUBSTRING(@AllSortSql, 20000, 24000); + PRINT SUBSTRING(@AllSortSql, 24000, 28000); + PRINT SUBSTRING(@AllSortSql, 28000, 32000); + PRINT SUBSTRING(@AllSortSql, 32000, 36000); + PRINT SUBSTRING(@AllSortSql, 36000, 40000); + END; + + EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT, @i_SkipAnalysis BIT, @i_OutputDatabaseName NVARCHAR(258), @i_OutputSchemaName NVARCHAR(258), @i_OutputTableName NVARCHAR(258), @i_CheckDateOverride DATETIMEOFFSET, @i_MinutesBack INT ', + @i_DatabaseName = @DatabaseName, @i_Top = @Top, @i_SkipAnalysis = @SkipAnalysis, @i_OutputDatabaseName = @OutputDatabaseName, @i_OutputSchemaName = @OutputSchemaName, @i_OutputTableName = @OutputTableName, @i_CheckDateOverride = @CheckDateOverride, @i_MinutesBack = @MinutesBack; + +/* Avoid going into OutputResultsToTable + ... otherwise the last result (e.g. spills) would be recorded twice into the output table. +*/ +RETURN; + +/*End of AllSort section*/ + + +/*Begin code to write results to table */ +OutputResultsToTable: + +RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; + +SELECT @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); + +/* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ +DECLARE @ValidOutputServer BIT; +DECLARE @ValidOutputLocation BIT; +DECLARE @LinkedServerDBCheck NVARCHAR(2000); +DECLARE @ValidLinkedServerDB INT; +DECLARE @tmpdbchk table (cnt int); +IF @OutputServerName IS NOT NULL + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; + + IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; +ELSE + BEGIN + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @StringToExecute NVARCHAR(MAX) = N'' ; + + IF @ValidOutputLocation = 1 + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + CONVERT + ( + nvarchar(MAX), + N'(ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,N'[',N''),N']',N'') + N'] PRIMARY KEY CLUSTERED(ID ASC));' + ); + + SET @StringToExecute += N'IF EXISTS(SELECT * FROM ' + +@OutputDatabaseName + +N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + +@OutputSchemaName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + +@OutputSchemaName + +N''' AND QUOTENAME(TABLE_NAME) = ''' + +@OutputTableName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.sys.computed_columns WHERE [name] = N''PlanCreationTimeHours'' AND QUOTENAME(OBJECT_NAME(object_id)) = N''' + +@OutputTableName + +N''' AND [definition] = N''(datediff(hour,[PlanCreationTime],sysdatetime()))'') +BEGIN + RAISERROR(''We noticed that you are running an old computed column definition for PlanCreationTimeHours, fixing that now'',0,0) WITH NOWAIT; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' DROP COLUMN [PlanCreationTimeHours]; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' ADD [PlanCreationTimeHours] AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]); +END '; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,'xml','nvarchar(max)'); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlySqlHandles = '''''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlyQueryHashes = '''''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''N/A''','''''N/A'''''); + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new LastCompletionTime column, add it. See Github #2377. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + ''')) AND name = ''LastCompletionTime'') + ALTER TABLE ' + @ObjectFullName + N' ADD LastCompletionTime DATETIME NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''LastCompletionTime''','''''LastCompletionTime'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new PlanGenerationNum column, add it. See Github #2514. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''PlanGenerationNum'') + ALTER TABLE ' + @ObjectFullName + N' ADD PlanGenerationNum BIGINT NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''PlanGenerationNum''','''''PlanGenerationNum'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new Pattern column, add it */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''Pattern'') + ALTER TABLE ' + @ObjectFullName + N' ADD Pattern NVARCHAR(20) NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''Pattern''','''''Pattern'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END + + IF @CheckDateOverride IS NULL + BEGIN + SET @CheckDateOverride = SYSDATETIMEOFFSET(); + END; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputServerName + '.' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputServerName + '.' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, CAST(QueryPlan AS NVARCHAR(MAX)), NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 1, 4000); + PRINT SUBSTRING(@StringToExecute, 4001, 4000); + PRINT SUBSTRING(@StringToExecute, 8001, 4000); + PRINT SUBSTRING(@StringToExecute, 12001, 4000); + PRINT SUBSTRING(@StringToExecute, 16001, 4000); + PRINT SUBSTRING(@StringToExecute, 20001, 4000); + PRINT SUBSTRING(@StringToExecute, 24001, 4000); + PRINT SUBSTRING(@StringToExecute, 28001, 4000); + PRINT SUBSTRING(@StringToExecute, 32001, 4000); + PRINT SUBSTRING(@StringToExecute, 36001, 4000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + ELSE + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + IF @ValidOutputServer = 1 + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF @OutputTableName IN ('##BlitzCacheProcs','##BlitzCacheResults') + BEGIN + RAISERROR('OutputTableName is a reserved name for this procedure. We only use ##BlitzCacheProcs and ##BlitzCacheResults, please choose another table name.', 16, 0); + END; + ELSE + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' + + 'CREATE TABLE ' + + @OutputTableName + + ' (ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,'[',''),']','') + '] PRIMARY KEY CLUSTERED(ID ASC));'; + SET @StringToExecute += N' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + + SET @StringToExecute += N' AND SPID = @@SPID '; + + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + PRINT SUBSTRING(@StringToExecute, 34000, 40000); + END; + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); +END; /* End of writing results to table */ + +END; /*Final End*/ + +GO +IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;'); +GO + + +ALTER PROCEDURE [dbo].[sp_BlitzFirst] + @LogMessage NVARCHAR(4000) = NULL , + @Help TINYINT = 0 , + @AsOf DATETIMEOFFSET = NULL , + @ExpertMode TINYINT = 0 , + @Seconds INT = 5 , + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(256) = NULL , + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputTableNameFileStats NVARCHAR(256) = NULL , + @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , + @OutputTableNameWaitStats NVARCHAR(256) = NULL , + @OutputTableNameBlitzCache NVARCHAR(256) = NULL , + @OutputTableNameBlitzWho NVARCHAR(256) = NULL , + @OutputResultSets NVARCHAR(500) = N'BlitzWho_Start|Findings|FileStats|PerfmonStats|WaitStats|BlitzCache|BlitzWho_End' , + @OutputTableRetentionDays TINYINT = 7 , + @OutputXMLasNVARCHAR TINYINT = 0 , + @FilterPlansByDatabase VARCHAR(MAX) = NULL , + @CheckProcedureCache TINYINT = 0 , + @CheckServerInfo TINYINT = 1 , + @FileLatencyThresholdMS INT = 100 , + @SinceStartup TINYINT = 0 , + @ShowSleepingSPIDs TINYINT = 0 , + @BlitzCacheSkipAnalysis BIT = 1 , + @MemoryGrantThresholdPct DECIMAL(5,2) = 15.00, + @LogMessageCheckID INT = 38, + @LogMessagePriority TINYINT = 1, + @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', + @LogMessageFinding VARCHAR(200) = 'Logged from sp_BlitzFirst', + @LogMessageURL VARCHAR(200) = '', + @LogMessageCheckDate DATETIMEOFFSET = NULL, + @Debug BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 + WITH EXECUTE AS CALLER, RECOMPILE +AS +BEGIN +SET NOCOUNT ON; +SET STATISTICS XML OFF; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +SELECT @Version = '8.26', @VersionDate = '20251002'; + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +IF @Help = 1 +BEGIN +PRINT ' +sp_BlitzFirst from http://FirstResponderKit.org + +This script gives you a prioritized list of why your SQL Server is slow right now. + +This is not an overall health check - for that, check out sp_Blitz. + +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. + +Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. It + may work just fine on 2005, and if it does, hug your parents. Just don''t + file support issues if it breaks. + - If a temp table called #CustomPerfmonCounters exists for any other session, + but not our session, this stored proc will fail with an error saying the + temp table #CustomPerfmonCounters does not exist. + - @OutputServerName is not functional yet. + - If @OutputDatabaseName, SchemaName, TableName, etc are quoted with brackets, + the write to table may silently fail. Look, I never said I was good at this. + +Unknown limitations of this version: + - None. Like Zombo.com, the only limit is yourself. + +Changes - for the full list of improvements and fixes in this version, see: +https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + + +MIT License + +Copyright (c) Brent Ozar Unlimited + +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. + +'; +RETURN; +END; /* @Help = 1 */ + +RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; +DECLARE @StringToExecute NVARCHAR(MAX), + @ParmDefinitions NVARCHAR(4000), + @Parm1 NVARCHAR(4000), + @OurSessionID INT, + @LineFeed NVARCHAR(10), + @StockWarningHeader NVARCHAR(MAX) = N'', + @StockWarningFooter NVARCHAR(MAX) = N'', + @StockDetailsHeader NVARCHAR(MAX) = N'', + @StockDetailsFooter NVARCHAR(MAX) = N'', + @StartSampleTime DATETIMEOFFSET, + @FinishSampleTime DATETIMEOFFSET, + @FinishSampleTimeWaitFor DATETIME, + @AsOf1 DATETIMEOFFSET, + @AsOf2 DATETIMEOFFSET, + @ServiceName sysname, + @OutputTableNameFileStats_View NVARCHAR(256), + @OutputTableNamePerfmonStats_View NVARCHAR(256), + @OutputTableNamePerfmonStatsActuals_View NVARCHAR(256), + @OutputTableNameWaitStats_View NVARCHAR(256), + @OutputTableNameWaitStats_Categories NVARCHAR(256), + @OutputTableCleanupDate DATE, + @ObjectFullName NVARCHAR(2000), + @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', + @BlitzCacheMinutesBack INT, + @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , + @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , + @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName , + @LocalServerName NVARCHAR(128) = CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), + @dm_exec_query_statistics_xml BIT = 0, + @total_cpu_usage BIT = 0, + @get_thread_time_ms NVARCHAR(MAX) = N'', + @thread_time_ms FLOAT = 0, + @logical_processors INT = 0, + @max_worker_threads INT = 0; + +/* Sanitize our inputs */ +SELECT + @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), + @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), + @OutputTableNamePerfmonStatsActuals_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Actuals'), + @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), + @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); + +SELECT + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName), + @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), + @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), + @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), + @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE), + /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ + /* @OutputTableNameBlitzWho = QUOTENAME(@OutputTableNameBlitzWho), We purposely don't sanitize this because sp_BlitzWho will */ + @LineFeed = CHAR(13) + CHAR(10), + @OurSessionID = @@SPID, + @OutputType = UPPER(@OutputType); + +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; +END; + +IF UPPER(@OutputType) LIKE 'TOP 10%' SET @OutputType = 'Top10'; +IF @OutputType = 'Top10' SET @SinceStartup = 1; + +/* Logged Message - CheckID 38 */ +IF @LogMessage IS NOT NULL + BEGIN + + RAISERROR('Saving LogMessage to table',10,1) WITH NOWAIT; + + /* Try to set the output table parameters if they don't exist */ + IF @OutputSchemaName IS NULL AND @OutputTableName IS NULL AND @OutputDatabaseName IS NULL + BEGIN + SET @OutputSchemaName = N'[dbo]'; + SET @OutputTableName = N'[BlitzFirst]'; + + /* Look for the table in the current database */ + SELECT TOP 1 @OutputDatabaseName = QUOTENAME(TABLE_CATALOG) + FROM INFORMATION_SCHEMA.TABLES + WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'BlitzFirst'; + + IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') + SET @OutputDatabaseName = '[DBAtools]'; + + END; + + IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL + OR NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; + RETURN; + END; + IF @LogMessageCheckDate IS NULL + SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ' + + ' @SrvName, @LogMessageCheckDate, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)'; + + EXECUTE sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', + @LocalServerName, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; + + RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; + + RETURN; + END; + +IF @SinceStartup = 1 + BEGIN + SET @Seconds = 0 + IF @ExpertMode = 0 + SET @ExpertMode = 1 + END; + + +IF @OutputType = 'SCHEMA' +BEGIN + SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)'; + +END; +ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL +BEGIN + /* They want to look into the past. */ + SET @AsOf1= DATEADD(mi, -15, @AsOf); + SET @AsOf2= DATEADD(mi, +15, @AsOf); + + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') SELECT CheckDate, [Priority], [FindingsGroup], [Finding], [URL], CAST([Details] AS [XML]) AS Details,' + + '[HowToStopIt], [CheckID], [StartTime], [LoginName], [NTUserName], [OriginalLoginName], [ProgramName], [HostName], [DatabaseID],' + + '[DatabaseName], [OpenTransactionCount], [QueryPlan], [QueryText] FROM ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' WHERE CheckDate >= @AsOf1' + + ' AND CheckDate <= @AsOf2' + + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; + EXEC sp_executesql @StringToExecute, + N'@AsOf1 DATETIMEOFFSET, @AsOf2 DATETIMEOFFSET', + @AsOf1, @AsOf2 + + +END; /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ +ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ +BEGIN + /* What's running right now? This is the first and last result set. */ + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_Start%' + BEGIN + IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL + BEGIN + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; + ELSE + BEGIN + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ + + /* Set start/finish times AFTER sp_BlitzWho runs. For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2244 */ + IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + /* Use the most accurate (but undocumented) DMV if it's available: */ + IF EXISTS(SELECT * FROM sys.all_columns ac WHERE ac.object_id = OBJECT_ID('sys.dm_cloud_database_epoch') AND ac.name = 'last_role_transition_time') + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),last_role_transition_time) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.dm_cloud_database_epoch; + ELSE + WITH WaitTimes AS ( + SELECT wait_type, wait_time_ms, + NTILE(3) OVER(ORDER BY wait_time_ms) AS grouper + FROM sys.dm_os_wait_stats w + WHERE wait_type IN ('DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', + 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT') + ) + SELECT @StartSampleTime = DATEADD(mi, AVG(-wait_time_ms / 1000 / 60), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() + FROM WaitTimes + WHERE grouper = 2; + END + ELSE IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.databases + WHERE database_id = 2; + ELSE + SELECT @StartSampleTime = SYSDATETIMEOFFSET(), + @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), + @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()); + + + SELECT @logical_processors = COUNT(*) + FROM sys.dm_os_schedulers + WHERE status = 'VISIBLE ONLINE'; + + IF EXISTS + ( + + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.dm_os_schedulers') + AND ac.name = 'total_cpu_usage_ms' + + ) + BEGIN + + SELECT + @total_cpu_usage = 1, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_cpu_usage_ms) + ) + FROM sys.dm_os_schedulers AS s + WHERE s.status = ''VISIBLE ONLINE'' + AND s.is_online = 1 + OPTION(RECOMPILE); + '; + + END + ELSE + BEGIN + SELECT + @total_cpu_usage = 0, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_worker_time / 1000.) + ) + FROM sys.dm_exec_query_stats AS s + OPTION(RECOMPILE); + '; + END + + RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; + + /* + We start by creating #BlitzFirstResults. It's a temp table that will store + the results from our checks. Throughout the rest of this stored procedure, + we're running a series of checks looking for dangerous things inside the SQL + Server. When we find a problem, we insert rows into the temp table. At the + end, we return these results to the end user. + + #BlitzFirstResults has a CheckID field, but there's no Check table. As we do + checks, we insert data into this table, and we manually put in the CheckID. + We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can + download that from http://FirstResponderKit.org if you want to build + a tool that relies on the output of sp_BlitzFirst. + */ + + + IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL + DROP TABLE #BlitzFirstResults; + CREATE TABLE #BlitzFirstResults + ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NULL, + Details NVARCHAR(MAX) NULL, + HowToStopIt NVARCHAR(MAX) NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + QueryStatsNowID INT NULL, + QueryStatsFirstID INT NULL, + PlanHandle VARBINARY(64) NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) + ); + + IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL + DROP TABLE #WaitStats; + CREATE TABLE #WaitStats ( + Pass TINYINT NOT NULL, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + thread_time_ms FLOAT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT, + SampleTime datetimeoffset + ); + + IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL + DROP TABLE #FileStats; + CREATE TABLE #FileStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass TINYINT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + avg_stall_read_ms INT , + avg_stall_write_ms INT + ); + + IF OBJECT_ID('tempdb..#QueryStats') IS NOT NULL + DROP TABLE #QueryStats; + CREATE TABLE #QueryStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass INT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + [sql_handle] VARBINARY(64), + statement_start_offset INT, + statement_end_offset INT, + plan_generation_num BIGINT, + plan_handle VARBINARY(64), + execution_count BIGINT, + total_worker_time BIGINT, + total_physical_reads BIGINT, + total_logical_writes BIGINT, + total_logical_reads BIGINT, + total_clr_time BIGINT, + total_elapsed_time BIGINT, + creation_time DATETIMEOFFSET, + query_hash BINARY(8), + query_plan_hash BINARY(8), + Points TINYINT + ); + + IF OBJECT_ID('tempdb..#PerfmonStats') IS NOT NULL + DROP TABLE #PerfmonStats; + CREATE TABLE #PerfmonStats ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + Pass TINYINT NOT NULL, + SampleTime DATETIMEOFFSET NOT NULL, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL + ); + + IF OBJECT_ID('tempdb..#PerfmonCounters') IS NOT NULL + DROP TABLE #PerfmonCounters; + CREATE TABLE #PerfmonCounters ( + ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL + ); + + IF OBJECT_ID('tempdb..#FilterPlansByDatabase') IS NOT NULL + DROP TABLE #FilterPlansByDatabase; + CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); + + IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL + DROP TABLE #checkversion; + CREATE TABLE #checkversion ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) + ); + + IF OBJECT_ID('tempdb..##WaitCategories') IS NULL + BEGIN + /* We reuse this one by default rather than recreate it every time. */ + CREATE TABLE ##WaitCategories + ( + WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, + WaitCategory NVARCHAR(128) NOT NULL, + Ignorable BIT DEFAULT 0 + ); + END; /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ + + IF 527 > (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) + BEGIN + TRUNCATE TABLE ##WaitCategories; + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_NETWORK_IO','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BACKUPIO','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_CONNECTION_RECEIVE_TASK','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_DISPATCHER','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_ENDPOINT_STATE_MUTEX','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_EVENTHANDLER','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_FORWARDER','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_INIT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_MASTERSTART','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_RECEIVE_WAITFOR','User Wait',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_REGISTERALLENDPOINTS','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SERVICE','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SHUTDOWN','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_START','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SHUTDOWN','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_STOP','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SUBMIT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TO_FLUSH','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_OBJECT','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_TABLE','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_WORK','Service Broker',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXCONSUMER','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_SEND','Mirroring',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_TABLE_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DPT_ENTRY_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_STATE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_TMDOWN_REQUEST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_WAITFOR_OUTCOME','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_ENLIST','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_PREPARE','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_RECOVERY','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TM','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TRANSACTION_ENLISTMENT','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCPNTSYNC','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EE_PMOLOCK','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXCHANGE','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXTERNAL_SCRIPT_NETWORK_IOF','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_READ','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_WRITE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_METADATA_MUTEX','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_PROPERTYLIST_CACHE','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_RESTART_CRAWL','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FULLTEXT GATHERER','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AG_MUTEX','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_CRITICAL_SECTION_ENTRY','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_MANAGER_MUTEX','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_UNLOAD_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_BULK_LOCK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_QUEUE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CLUSAPI_CALL','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_COMPRESSED_CACHE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CONNECTIVITY_INFO','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_VERSIONING_STATE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RECOVERY','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RESTART','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_COMMAND','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_COMPLETION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_START_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER_FILTER_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_MANAGER','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_PREPROC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_GROUP_COMMIT','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_WAIT','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGPROGRESS_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_DEQUEUE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_STARTUP_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_TERMINATION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_PARTNER_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_READ_ALL_NETWORKS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_CONNECTION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_UNDO','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_REPLICAINFO_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_CANCELLATION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_FILE_LIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_LIMIT_BACKUPS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_SYNC_COMPLETION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_TIMEOUT_TASK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_WAIT_FOR_COMPLETION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNC_COMMIT','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNCHRONIZING_THROTTLE','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC_PROCESSING','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_THROTTLE_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TIMER_TASK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_DBRLIST','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_SESSION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_POOL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_QUEUE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_XRF_STACK_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('INSTANCE_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_QUEUE_LIMIT','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_RETRY','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_DT','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_EX','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_KP','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_NL','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_SH','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_UP','Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LAZYWRITER_SLEEP','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOG_RATE_GOVERNOR','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_PMM_LOG','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_RESERVE_APPEND','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_ALLOCATION_EXT','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_GRANT_UPDATE','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MGR_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSSEARCH','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('NET_WAITFOR_PACKET','Network IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ONDEMAND_TASK_QUEUE','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_DT','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_EX','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_KP','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_NL','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_SH','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_UP','Buffer IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_DT','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_EX','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_KP','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_DRAIN_WORKER','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_LOG_CACHE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_LIST','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_TURN','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_SYNC','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_WAIT_WORK','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POPULATE_LOCK_ORDINALS','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPVDIDEVICE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COCREATEINSTANCE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COGETCLASSOBJECT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_CREATEACCESSOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_DELETEROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETCOMMANDTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETNEXTROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETRESULT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETROWSBYBOOKMARK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBFLUSH','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBREADAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSETSIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSTAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBUNLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBWRITEAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_QUERYINTERFACE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEACCESSOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEROWS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASESESSION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RESTARTPOSITION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREADANDWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETDATAFAILURE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSETSIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSTAT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMUNLOCKREGION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CONSOLEWRITE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CREATEPARAM','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DEBUG','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSADDLINK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKEXISTCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKHEALTHCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVELINK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVEROOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTFOLDERCHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTSHARECHECK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORTREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_BEGINTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_COMMITREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ENLIST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_PREPAREREQUESTDONE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FILESIZEGET','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_STARTTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_GETRMINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_EVENT_WAIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_REQUEST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_LOCKMONITOR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_MSS_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ODBCOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLE_UNINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTTRAN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETDATASOURCE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETLITERALINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTYINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETSCHEMALOCK','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_JOINTRANSACTION','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_RELEASE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_SETPROPERTIES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDBOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHENTICATIONOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHORIZATIONOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_BACKUPREAD','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLOSEHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLUSTEROPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMPLETEAUTHTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COPYFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTACQUIRECONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTIMPORTKEY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DECRYPTMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETESECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEIOCONTROL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DIRSVC_NETWORKOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DISCONNECTNAMEDPIPE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DOMAINSERVICESOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DSGETDCNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DTCOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ENCRYPTMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FILEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FINDFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FORMATMESSAGE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREECREDENTIALSHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREELIBRARY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GENERICOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETADDRINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETDISKFREESPACE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILEATTRIBUTES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILESIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETLONGPATHNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETPROCADDRESS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMEPATHNAME','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LIBRARYOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOADLIBRARY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOGONUSER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOOKUPACCOUNTSID','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MESSAGEQUEUEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MOVEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETGROUPGETUSERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETGROUPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETLOCALGROUPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERMODALSGET','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_OPENDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PDH_WMI_INIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PIPEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PROCESSOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYREGISTRY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REMOVEDIRECTORY','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REPORTEVENT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REVERTTOSELF','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_RSFXDEVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SECURITYOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SERVICEOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETENDOFFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEPOINTER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEVALIDDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETNAMEDSECURITYINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQLCLROPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQMLAUNCH','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYSIGNATURE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYTRUST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VSSOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WAITFORSINGLEOBJECT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WINSOCKOPS','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILEGATHER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WSASETLASTERROR','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_REENLIST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_RESIZELOG','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDREDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDUNDO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SB_STOPENDPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SERVER_STARTUP','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SETRMINFO','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SHAREDMEM_GETDATA','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SNIOPEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSHOST','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSTESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STARTRM','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_CHECKPOINT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_RECOVER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STRESSDRIVER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TRANSIMPORT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATESNAPSHOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETFINALIZE','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETINIT','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TIMERRUN','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XETESTING','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ACTION_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CLUSTER_INTEGRATION','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_FAILOVER_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_JOIN','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_OFFLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ONLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_POST_ONLINE_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_SERVER_READY_CONNECTIONS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_WORKITEM_COMPLETED','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADRSIM','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_ASYNC_QUEUE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_SHUTDOWN_QUEUE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QUERY_TRACEOUT','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REDO_THREAD_PENDING_WORK','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_CACHE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_HISTORYCACHE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_SCHEMA_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANFSINFO_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANHASHTABLE_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANTEXTINFO_ACCESS','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPLICA_WRITES','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REQUEST_FOR_DEADLOCK_SEARCH','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESERVED_MEMORY_ALLOCATION_EXT','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE','Memory',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE_QUERY_COMPILE','Compilation',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BPOOL_FLUSH','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BUFFERPOOL_HELPLW','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DCOMSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERDBREADY','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERMDREADY','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERUPGRADED','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MEMORYPOOL_ALLOCATEPAGES','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MSDBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_RETRY_VIRTUALALLOC','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_SYSTEMTASK','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TASK','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_WORK_DISPATCHER','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_DEADLOCK_DETECTION','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_QUANTUM_PUNISHMENT','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_BUFFER_FLUSH','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_BUFFER','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_READ_IO_COMPLETION','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_WRITE_IO_COMPLETION','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_PENDING_BUFFER_WRITERS','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_SHUTDOWN','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_WAIT_ENTRIES','Idle',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('THREADPOOL','Worker Thread',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACE_EVTNOTIF','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACEWRITE','Tracing',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_DT','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_EX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_KP','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_NL','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_SH','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_UP','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRANSACTION_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('UCS_SESSION_REGISTRATION','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_FOR_RESULTS','User Wait',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAITFOR','User Wait',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITE_COMPLETION','Other Disk IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITELOG','Tran Log IO',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_OWN_TRANSACTION','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_RECLAIM_SESSION','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTLOCKINFO','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTWORKSPACE_MUTEX','Transaction',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); + END; /* IF SELECT SUM(1) FROM ##WaitCategories <> 527 */ + + + + IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL + DROP TABLE #MasterFiles; + CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); + /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ + IF (SERVERPROPERTY('EngineEdition') = 5 /*(SERVERPROPERTY('Edition')) = 'SQL Azure' */ + AND (OBJECT_ID('sys.master_files') IS NULL)) + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;'; + ELSE + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;'; + EXEC(@StringToExecute); + + IF @FilterPlansByDatabase IS NOT NULL + BEGIN + IF UPPER(LEFT(@FilterPlansByDatabase,4)) = 'USER' + BEGIN + INSERT INTO #FilterPlansByDatabase (DatabaseID) + SELECT database_id + FROM sys.databases + WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb'); + END; + ELSE + BEGIN + SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' + ;WITH a AS + ( + SELECT CAST(1 AS BIGINT) f, CHARINDEX(',', @FilterPlansByDatabase) t, 1 SEQ + UNION ALL + SELECT t + 1, CHARINDEX(',', @FilterPlansByDatabase, t + 1), SEQ + 1 + FROM a + WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 + ) + INSERT #FilterPlansByDatabase (DatabaseID) + SELECT DISTINCT db.database_id + FROM a + INNER JOIN sys.databases db ON LTRIM(RTRIM(SUBSTRING(@FilterPlansByDatabase, a.f, a.t - a.f))) = db.name + WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL + OPTION (MAXRECURSION 0); + END; + END; + + IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + CREATE TABLE #ReadableDBs ( + database_id INT + ); + + IF 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; + + SET @StringToExecute = 'INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'; + EXEC(@StringToExecute); + + END + + DECLARE @v DECIMAL(6,2), + @build INT, + @memGrantSortSupported BIT = 1; + + RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; + + INSERT INTO #checkversion (version) + SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + OPTION (RECOMPILE); + + + SELECT @v = common_version , + @build = build + FROM #checkversion + OPTION (RECOMPILE); + + IF (@v < 11) + OR (@v = 11 AND @build < 6020) + OR (@v = 12 AND @build < 5000) + OR (@v = 13 AND @build < 1601) + SET @memGrantSortSupported = 0; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + AND ((@v = 13 AND @build >= 5337) /* This DMF causes assertion errors: https://support.microsoft.com/en-us/help/4490136/fix-assertion-error-occurs-when-you-use-sys-dm-exec-query-statistics-x */ + OR (@v = 14 AND @build >= 3162) + OR (@v >= 15) + OR (@v <= 12)) /* Azure */ + SET @dm_exec_query_statistics_xml = 1; + + + SET @StockWarningHeader = '', + @StockDetailsHeader = @StockDetailsHeader + ''; + + /* Get the instance name to use as a Perfmon counter prefix. */ + IF SERVERPROPERTY('EngineEdition') IN (5, 8) /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure'*/ + SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) + FROM sys.dm_os_performance_counters; + ELSE + BEGIN + SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;'; + EXEC(@StringToExecute); + SELECT @ServiceName = object_name FROM #PerfmonStats; + DELETE #PerfmonStats; + END; + + /* Build a list of queries that were run in the last 10 seconds. + We're looking for the death-by-a-thousand-small-cuts scenario + where a query is constantly running, and it doesn't have that + big of an impact individually, but it has a ton of impact + overall. We're going to build this list, and then after we + finish our @Seconds sample, we'll compare our plan cache to + this list to see what ran the most. */ + + /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ + IF @CheckProcedureCache = 1 + BEGIN + RAISERROR('@CheckProcedureCache = 1, capturing first pass of plan cache',10,1) WITH NOWAIT; + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) + AND attr.attribute = ''dbid'';'; + END; + END; + ELSE + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) + AND attr.attribute = ''dbid'';'; + END; + END; + EXEC(@StringToExecute); + + /* Get the totals for the entire plan cache */ + INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) + SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) + FROM sys.dm_exec_query_stats qs; + END; /*IF @CheckProcedureCache = 1 */ + + + IF EXISTS (SELECT * + FROM tempdb.sys.all_objects obj + INNER JOIN tempdb.sys.all_columns col1 ON obj.object_id = col1.object_id AND col1.name = 'object_name' + INNER JOIN tempdb.sys.all_columns col2 ON obj.object_id = col2.object_id AND col2.name = 'counter_name' + INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' + WHERE obj.name LIKE '%CustomPerfmonCounters%') + BEGIN + SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters'; + EXEC(@StringToExecute); + END; + ELSE + BEGIN + /* Add our default Perfmon counters */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Group','Active Hadr Threads','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Received from Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Transport/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control Time (ms/sec)','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Resent Messages/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Sends to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Control Delay', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Controls/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commit Time', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commits/Sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Pending Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Ready Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Compression Cache misses/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log remaining for undo', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Send Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Recovery Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo blocked/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo Bytes Remaining', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redone Bytes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Bytes Flushed/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Pool LogWriter Pushes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL); + /* Below counters added by Jefferson Elias */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL); + /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. + And yes, they actually hard-coded the version numbers into the counters, and SQL 2019 still says 2017, oddly. + For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group + */ + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions created/sec',NULL); + END; + + + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END + + /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. + After we finish doing our checks, we'll take another sample and compare them. */ + RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + CASE @Seconds + WHEN 0 + THEN 0 + ELSE @thread_time_ms + END AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + owt.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + 0 AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + os.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; + + SET @StringToExecute = @StringToExecute + N' + ) x + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 + ) + GROUP BY x.Pass, x.SampleTime, x.wait_type + ORDER BY sum_wait_time_ms DESC;' + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 1 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 1 + OPTION(RECOMPILE); + + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , + num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + mf.[database_id], + mf.[file_id], + DB_NAME(vfs.database_id) AS [db_name], + mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , + CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_read_ms END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_reads END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_read] END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_write_ms END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_writes END , + CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_written] END , + mf.physical_name, + mf.type_desc + FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs + INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id + AND vfs.database_id = mf.database_id + WHERE vfs.num_of_reads > 0 + OR vfs.num_of_writes > 0; + + INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) + SELECT 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), CASE @Seconds WHEN 0 THEN 0 ELSE dmv.cntr_value END, dmv.cntr_type + FROM #PerfmonCounters counters + INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS + AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); + + /* For Github #2743: */ + CREATE TABLE #TempdbOperationalStats (object_id BIGINT PRIMARY KEY CLUSTERED, + forwarded_fetch_count BIGINT); + INSERT INTO #TempdbOperationalStats (object_id, forwarded_fetch_count) + SELECT object_id, forwarded_fetch_count + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID('tempdb'), NULL, NULL, NULL) os + WHERE os.database_id = DB_ID('tempdb') + AND os.forwarded_fetch_count > 100; + + + /* If they want to run sp_BlitzWho and export to table, go for it. */ + IF @OutputTableNameBlitzWho IS NOT NULL + AND @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('Logging sp_BlitzWho to table',10,1) WITH NOWAIT; + EXEC sp_BlitzWho @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzWho, @CheckDateOverride = @StartSampleTime, @OutputTableRetentionDays = @OutputTableRetentionDays; + END + + RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; + + + /* Maintenance Tasks Running - Backup Running - CheckID 1 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 1',10,1) WITH NOWAIT; + END + + IF EXISTS(SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 300000 AND command LIKE 'BACKUP%') + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 1 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'Backup Running' AS Finding, + 'https://www.brentozar.com/askbrent/backups/' AS URL, + 'Backup 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) ' + @LineFeed + + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' + @LineFeed + + CASE WHEN COALESCE(s.nt_username, s.loginame) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_username, s.loginame) + ' ') ELSE '' END AS Details, + 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.[hostname] AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) 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.sysprocesses AS s ON r.session_id = s.spid AND s.ecid = 0 + INNER JOIN + ( + SELECT DISTINCT + t.request_session_id, + t.resource_database_id + FROM sys.dm_tran_locks AS t + WHERE t.resource_type = N'DATABASE' + AND t.request_mode = N'S' + AND t.request_status = N'GRANT' + AND t.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE' + ) AS db ON s.spid = db.request_session_id AND s.dbid = db.resource_database_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'BACKUP%' + AND r.start_time <= DATEADD(minute, -5, GETDATE()) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END + + /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ + IF @Seconds > 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure'*/ + BEGIN + SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; + EXEC(@StringToExecute); + END; + + + /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 2',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 2 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'DBCC CHECK* Running' AS Finding, + 'https://www.brentozar.com/askbrent/dbcc/' AS URL, + 'Corruption check 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) 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, + s.login_name AS LoginName, + 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, + 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id + FROM sys.dm_tran_locks l + INNER JOIN sys.databases d ON l.resource_database_id = d.database_id + WHERE l.resource_type = N'DATABASE' + AND l.request_mode = N'S' + AND l.request_status = N'GRANT' + AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) pl + OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t + WHERE r.command LIKE 'DBCC%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%ALTER INDEX%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%fileproperty%' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END + + /* Maintenance Tasks Running - Restore Running - CheckID 3 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 3',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'RESTORE%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 3 AS CheckID, + 1 AS Priority, + '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), + (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, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + 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 + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'RESTORE%' + AND s.program_name <> 'SQL Server Log Shipping' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END + + /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 4',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) + SELECT 4 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Database File Growing' AS Finding, + 'https://www.brentozar.com/go/instant' AS URL, + 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, + 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out https://www.brentozar.com/go/instant for details.' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + NULL AS DatabaseID, + NULL AS DatabaseName, + 0 AS OpenTransactionCount + FROM sys.dm_os_waiting_tasks t + INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id + INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END + + /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 5',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 5 AS CheckID, + 1 AS Priority, + ''Query Problems'' AS FindingGroup, + ''Long-Running Query Blocking Others'' AS Finding, + ''https://www.brentozar.com/go/blocking'' AS URL, + ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows: ' + + @LineFeed + @LineFeed + + '''+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, + ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, + (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, + COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), + (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id)) AS QueryText, + r.start_time AS StartTime, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + r.[database_id] AS DatabaseID, + DB_NAME(r.database_id) AS DatabaseName, + 0 AS OpenTransactionCount, + r.query_hash + FROM sys.dm_os_waiting_tasks tBlocked + INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id + LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id + INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id + WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000 + /* And the blocking session ID is not blocked by anyone else: */ + AND NOT EXISTS(SELECT * FROM sys.dm_os_waiting_tasks tBlocking WHERE s.session_id = tBlocking.session_id AND tBlocking.session_id <> tBlocking.blocking_session_id AND tBlocking.blocking_session_id IS NOT NULL) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs);'; + EXECUTE sp_executesql @StringToExecute; + END; + + /* Query Problems - Plan Cache Erased Recently - CheckID 7 */ + IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 7',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT TOP 1 7 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Plan Cache Erased Recently' AS Finding, + 'https://www.brentozar.com/askbrent/plan-cache-erased-recently/' AS URL, + 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed + + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed + + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed + + 'SQL Server has to use a lot of CPU power in order to build execution' + @LineFeed + + 'plans and put them in cache again. This causes high CPU loads.' AS Details, + 'Find who did that, and stop them from doing it again.' AS HowToStopIt + FROM sys.dm_exec_query_stats + ORDER BY creation_time; + END; + + + /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 8',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 8 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Sleeping Query with Open Transactions' AS Finding, + 'https://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, + 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.hostname + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_batch AS NVARCHAR(100)) + '. ' AS Details, + 'KILL ' + CAST(s.spid AS NVARCHAR(100)) + ';' AS HowToStopIt, + s.last_batch AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.hostname AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) AS DatabaseName, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + s.open_tran AS OpenTransactionCount + FROM sys.sysprocesses s + INNER JOIN sys.dm_exec_connections c ON s.spid = c.session_id + INNER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.spid = db.request_session_id + WHERE s.status = 'sleeping' + AND s.open_tran > 0 + AND s.last_batch < DATEADD(ss, -10, SYSDATETIME()) + AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.spid + AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')); + END + + /*Query Problems - Clients using implicit transactions - CheckID 37 */ + IF @Seconds > 0 + AND ( @@VERSION NOT LIKE 'Microsoft SQL Server 2005%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008 R2%' ) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 37',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 37 AS CheckId, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Implicit Transactions'', + ''https://www.brentozar.com/go/ImplicitTransactions/'' AS URL, + ''Database: '' + DB_NAME(s.database_id) + '' '' + CHAR(13) + CHAR(10) + + ''Host: '' + s.[host_name] + '' '' + CHAR(13) + CHAR(10) + + ''Program: '' + s.[program_name] + '' '' + CHAR(13) + CHAR(10) + + CONVERT(NVARCHAR(10), s.open_transaction_count) + + '' open transactions since: '' + + CONVERT(NVARCHAR(30), tat.transaction_begin_time) + ''. '' + AS Details, + ''Run sp_BlitzWho and check the is_implicit_transaction column to spot the culprits. +If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, + tat.transaction_begin_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + s.database_id, + DB_NAME(s.database_id) AS DatabaseName, + NULL AS Querytext, + s.open_transaction_count AS OpenTransactionCount + FROM sys.dm_tran_active_transactions AS tat + LEFT JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + LEFT JOIN sys.dm_exec_sessions AS s + ON s.session_id = tst.session_id + WHERE tat.name = ''implicit_transaction''; + ' + EXECUTE sp_executesql @StringToExecute; + END; + + /* Query Problems - Query Rolling Back - CheckID 9 */ + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 9',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, QueryHash) + SELECT 9 AS CheckID, + 1 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query Rolling Back' AS Finding, + 'https://www.brentozar.com/askbrent/rollback/' AS URL, + 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, + 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, + r.start_time AS StartTime, + s.login_name AS LoginName, + 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, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + r.query_hash + FROM sys.dm_exec_sessions s + INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + WHERE r.status = 'rollback'; + END + + IF @Seconds > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 1 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END + + /* Server Performance - Too Much Free Memory - CheckID 34 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 34',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 34 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Too Much Free Memory' AS Finding, + 'https://www.brentozar.com/go/freememory' AS URL, + CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, + 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt + FROM sys.dm_os_performance_counters cFree + INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' + AND cTotal.counter_name = N'Total Server Memory (KB) ' + WHERE cFree.object_name LIKE N'%Memory Manager%' + AND cFree.counter_name = N'Free Memory (KB) ' + AND CAST(cFree.cntr_value AS BIGINT) > 20480000000 + AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) + AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; + + /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 35',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 35 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Target Memory Lower Than Max' AS Finding, + 'https://www.brentozar.com/go/target' AS URL, + N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed + + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, + 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt + FROM sys.configurations cMax + INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' + AND cTarget.counter_name = N'Target Server Memory (KB) ' + WHERE cMax.name = 'max server memory (MB)' + AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) + AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ + AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ + END + + /* Server Info - Database Size, Total GB - CheckID 21 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 21',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 21 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Database Size, Total GB' AS Finding, + CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, + SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM #MasterFiles + WHERE database_id > 4; + + /* Server Info - Database Count - CheckID 22 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 22',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 22 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Database Count' AS Finding, + CAST(SUM(1) AS VARCHAR(100)) AS Details, + SUM (1) AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM sys.databases + WHERE database_id > 4; + + + /* Server Info - Memory Grants pending - CheckID 39 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 39',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 39 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Grants Pending' AS Finding, + CAST(PendingGrants.Details AS NVARCHAR(50)) AS Details, + PendingGrants.DetailsInt, + 'https://www.brentozar.com/blitz/memory-grants/' AS URL + FROM + ( + SELECT + COUNT(1) AS Details, + COUNT(1) AS DetailsInt + FROM sys.dm_exec_query_memory_grants AS Grants + WHERE queue_id IS NOT NULL + ) AS PendingGrants + WHERE PendingGrants.Details > 0; + + /* Server Info - Memory Grant/Workspace info - CheckID 40 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 40',10,1) WITH NOWAIT; + END + + DECLARE @MaxWorkspace BIGINT + SET @MaxWorkspace = (SELECT CAST(cntr_value AS BIGINT)/1024 FROM #PerfmonStats WHERE counter_name = N'Maximum Workspace Memory (KB)') + + IF (@MaxWorkspace IS NULL + OR @MaxWorkspace = 0) + BEGIN + SET @MaxWorkspace = 1 + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 40 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Memory Grant/Workspace info' AS Finding, + + 'Grants Outstanding: ' + CAST((SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS NVARCHAR(50)) + @LineFeed + + 'Total Granted(MB): ' + CAST(ISNULL(SUM(Grants.granted_memory_kb) / 1024, 0) AS NVARCHAR(50)) + @LineFeed + + 'Total WorkSpace(MB): ' + CAST(ISNULL(@MaxWorkspace, 0) AS NVARCHAR(50)) + @LineFeed + + 'Granted workspace: ' + CAST(ISNULL((CAST(SUM(Grants.granted_memory_kb) / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + @LineFeed + + 'Oldest Grant in seconds: ' + CAST(ISNULL(DATEDIFF(SECOND, MIN(Grants.request_time), GETDATE()), 0) AS NVARCHAR(50)) AS Details, + (SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM sys.dm_exec_query_memory_grants AS Grants; + + /* Query Problems - Queries with high memory grants - CheckID 46 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 46',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL, QueryText, QueryPlan) + SELECT 46 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query with a memory grant exceeding ' + +CAST(@MemoryGrantThresholdPct AS NVARCHAR(15)) + +'%' AS Finding, + 'Granted size: '+ CAST(CAST(Grants.granted_memory_kb / 1024 AS INT) AS NVARCHAR(50)) + +N'MB ' + + @LineFeed + +N'Granted pct of max workspace: ' + + CAST(ISNULL((CAST(Grants.granted_memory_kb / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + + @LineFeed + +N'SQLHandle: ' + +CONVERT(NVARCHAR(128),Grants.[sql_handle],1), + 'https://www.brentozar.com/memory-grants-sql-servers-public-toilet/' AS URL, + SQLText.[text], + QueryPlan.query_plan + FROM sys.dm_exec_query_memory_grants AS Grants + OUTER APPLY sys.dm_exec_sql_text(Grants.[sql_handle]) AS SQLText + OUTER APPLY sys.dm_exec_query_plan(Grants.[plan_handle]) AS QueryPlan + WHERE Grants.granted_memory_kb > ((@MemoryGrantThresholdPct/100.00)*(@MaxWorkspace*1024)); + + /* Query Problems - Memory Leak in USERSTORE_TOKENPERM Cache - CheckID 45 */ + IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_os_memory_clerks') AND name = 'pages_kb') + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45',10,1) WITH NOWAIT; + END + + /* SQL 2012+ version */ + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / SUM(pages_kb) >= 0.1 + AND SUM(pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + ELSE + BEGIN + /* Antiques Roadshow SQL 2008R2 - version */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45 (Legacy version)',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / SUM(single_pages_kb + multi_pages_kb) >= 0.1 + AND SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + + + + IF @Seconds > 0 + BEGIN + + IF EXISTS ( SELECT 1/0 + FROM sys.all_objects AS ao + WHERE ao.name = 'dm_exec_query_profiles' ) + BEGIN + + IF EXISTS( SELECT 1/0 + FROM sys.dm_exec_requests AS r + JOIN sys.dm_exec_sessions AS s + ON r.session_id = s.session_id + WHERE s.host_name IS NOT NULL + AND r.total_elapsed_time > 5000 + AND r.request_id > 0 ) + BEGIN + + SET @StringToExecute = N' + DECLARE @bad_estimate TABLE + ( + session_id INT, + request_id INT, + estimate_inaccuracy BIT + ); + + INSERT @bad_estimate ( session_id, request_id, estimate_inaccuracy ) + SELECT x.session_id, + x.request_id, + x.estimate_inaccuracy + FROM ( + SELECT deqp.session_id, + deqp.request_id, + CASE WHEN (deqp.row_count/10000) > deqp.estimate_row_count + THEN 1 + ELSE 0 + END AS estimate_inaccuracy + FROM sys.dm_exec_query_profiles AS deqp + INNER JOIN sys.dm_exec_requests r ON deqp.session_id = r.session_id AND deqp.request_id = r.request_id + WHERE deqp.session_id <> @@SPID + AND r.total_elapsed_time > 5000 + ) AS x + WHERE x.estimate_inaccuracy = 1 + GROUP BY x.session_id, + x.request_id, + x.estimate_inaccuracy; + + DECLARE @parallelism_skew TABLE + ( + session_id INT, + request_id INT, + parallelism_skew BIT + ); + + INSERT @parallelism_skew ( session_id, request_id, parallelism_skew ) + SELECT y.session_id, + y.request_id, + y.parallelism_skew + FROM ( + SELECT x.session_id, + x.request_id, + x.node_id, + x.thread_id, + x.row_count, + x.sum_node_rows, + x.node_dop, + x.sum_node_rows / x.node_dop AS even_distribution, + x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) AS skew_percent, + CASE + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) > 2. + THEN 1 + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) < 0.5 + THEN 1 + ELSE 0 + END AS parallelism_skew + FROM ( + SELECT deqp.session_id, + deqp.request_id, + deqp.node_id, + deqp.thread_id, + deqp.row_count, + SUM(deqp.row_count) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS sum_node_rows, + COUNT(*) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS node_dop + FROM sys.dm_exec_query_profiles AS deqp + WHERE deqp.thread_id > 0 + AND deqp.session_id <> @@SPID + AND EXISTS + ( + SELECT 1/0 + FROM sys.dm_exec_query_profiles AS deqp2 + WHERE deqp.session_id = deqp2.session_id + AND deqp.node_id = deqp2.node_id + AND deqp2.thread_id > 0 + GROUP BY deqp2.session_id, deqp2.node_id + HAVING COUNT(deqp2.node_id) > 1 + ) + ) AS x + ) AS y + WHERE y.parallelism_skew = 1 + GROUP BY y.session_id, + y.request_id, + y.parallelism_skew; + + /* Queries in dm_exec_query_profiles showing signs of poor cardinality estimates - CheckID 42 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 42'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 42 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x cardinality misestimations'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(b.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a large cardinality misestimate'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @bad_estimate AS b + JOIN sys.dm_exec_requests AS r + ON r.session_id = b.session_id + AND r.request_id = b.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = b.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + /* GitHub #3210 */ + SET @StringToExecute = N' + SET LOCK_TIMEOUT 1000 ' + @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + SET @StringToExecute = @StringToExecute + N'; + + /* Queries in dm_exec_query_profiles showing signs of unbalanced parallelism - CheckID 43 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 43'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 43 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x skewed parallelism'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(p.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a parallel threads doing uneven work.'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @parallelism_skew AS p + JOIN sys.dm_exec_requests AS r + ON r.session_id = p.session_id + AND r.request_id = p.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = p.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + SET @StringToExecute = @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + + SET @StringToExecute = @StringToExecute + N';'; + + EXECUTE sp_executesql @StringToExecute, N'@Debug BIT',@Debug = @Debug; + END + + END + END + + /* Server Performance - High CPU Utilization - CheckID 24 */ + IF @Seconds < 30 + BEGIN + /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. + We get this data from the ring buffers, and it's only updated once per minute, so might + as well get it now - whereas if we're checking 30+ seconds, it might get updated by the + end of our sp_BlitzFirst session. */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; + 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'%.', 100 - SystemIdle, 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; + + /* CPU Utilization - CheckID 23 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + END + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + WITH y + AS + ( + SELECT CONVERT(VARCHAR(5), 100 - ca.c.value('.', 'INT')) AS system_idle, + CONVERT(VARCHAR(30), rb.event_date) AS event_date, + CONVERT(VARCHAR(8000), rb.record) AS record, + event_date as event_date_raw + FROM + ( SELECT CONVERT(XML, dorb.record) AS record, + DATEADD(ms, -( ts.ms_ticks - dorb.timestamp ), GETDATE()) AS event_date + FROM sys.dm_os_ring_buffers AS dorb + CROSS JOIN + ( SELECT dosi.ms_ticks FROM sys.dm_os_sys_info AS dosi ) AS ts + 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) + ) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL, HowToStopIt) + SELECT TOP 1 + 23, + 250, + 'Server Info', + 'CPU Utilization', + y.system_idle + N'%. Ring buffer details: ' + CAST(y.record AS NVARCHAR(4000)), + y.system_idle , + 'https://www.brentozar.com/go/cpu', + STUFF(( SELECT TOP 2147483647 + CHAR(10) + CHAR(13) + + y2.system_idle + + '% ON ' + + y2.event_date + + ' Ring buffer details: ' + + y2.record + FROM y AS y2 + ORDER BY y2.event_date_raw DESC + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'VARCHAR(MAX)'), 1, 1, N'') AS query + FROM y + ORDER BY y.event_date_raw DESC; + + + /* Highlight if non SQL processes are using >25% CPU - CheckID 28 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 28',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'https://www.brentozar.com/go/cpu' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + ,record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLUsage + 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 - (y.SQLUsage + y.SystemIdle) >= 25; + + END; /* IF @Seconds < 30 */ + + /* Query Problems - Statistics Updated Recently - CheckID 44 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 44',10,1) WITH NOWAIT; + END + + IF 20 >= (SELECT COUNT(*) FROM sys.databases WHERE name NOT IN ('master', 'model', 'msdb', 'tempdb')) + AND @Seconds > 0 + BEGIN + CREATE TABLE #UpdatedStats (HowToStopIt NVARCHAR(4000), RowsForSorting BIGINT); + IF EXISTS(SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_properties') + BEGIN + /* We don't want to hang around to obtain locks */ + SET LOCK_TIMEOUT 0; + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SET @StringToExecute = N'USE [?];' + @LineFeed; + ELSE + SET @StringToExecute = N''; + + SET @StringToExecute = @StringToExecute + 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET LOCK_TIMEOUT 1000;' + @LineFeed + + 'BEGIN TRY' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) + N''.'' +' + @LineFeed + + ' QUOTENAME(SCHEMA_NAME(obj.schema_id)) + N''.'' +' + @LineFeed + + ' QUOTENAME(obj.name) +' + @LineFeed + + ' N'' statistic '' + QUOTENAME(stat.name) + ' + @LineFeed + + ' N'' was updated on '' + CONVERT(NVARCHAR(50), sp.last_updated, 121) + N'','' + ' + @LineFeed + + ' N'' had '' + CAST(sp.rows AS NVARCHAR(50)) + N'' rows, with '' +' + @LineFeed + + ' CAST(sp.rows_sampled AS NVARCHAR(50)) + N'' rows sampled,'' + ' + @LineFeed + + ' N'' producing '' + CAST(sp.steps AS NVARCHAR(50)) + N'' steps in the histogram.'',' + @LineFeed + + ' sp.rows' + @LineFeed + + ' FROM sys.objects AS obj WITH (NOLOCK)' + @LineFeed + + ' INNER JOIN sys.stats AS stat WITH (NOLOCK) ON stat.object_id = obj.object_id ' + @LineFeed + + ' CROSS APPLY sys.dm_db_stats_properties(stat.object_id, stat.stats_id) AS sp ' + @LineFeed + + ' WHERE sp.last_updated > DATEADD(MI, -15, GETDATE())' + @LineFeed + + ' AND obj.is_ms_shipped = 0' + @LineFeed + + ' AND ''[?]'' <> ''[tempdb]'';' + @LineFeed + + 'END TRY' + @LineFeed + + 'BEGIN CATCH' + @LineFeed + + ' IF (ERROR_NUMBER() = 1222)' + @LineFeed + + ' BEGIN ' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as the lock timeout was exceeded,''+' + @LineFeed + + ' N'' this is likely due to an Index operation in Progress'',' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + ' ELSE' + @LineFeed + + ' BEGIN' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as a result of error: ''+' + @LineFeed + + ' CAST(ERROR_NUMBER() AS NVARCHAR(10)) +' + @LineFeed + + ' N'' with message: ''+' + @LineFeed + + ' CAST(ERROR_MESSAGE() AS NVARCHAR(128)),' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + 'END CATCH' + ; + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + BEGIN TRY + EXEC sp_MSforeachdb @StringToExecute; + END TRY + BEGIN CATCH + IF (ERROR_NUMBER() = 1222) + BEGIN + INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting) + SELECT HowToStopIt = N'No information could be retrieved as the lock timeout was exceeded while iterating databases,' + + N' this is likely due to an Index operation in Progress', -1; + END + ELSE + BEGIN + THROW; + END + END CATCH + END + ELSE + EXEC(@StringToExecute); + + /* Set timeout back to a default value of -1 */ + SET LOCK_TIMEOUT -1; + END; + + /* We mark timeout exceeded with a -1 so only show these IF there is statistics info that succeeded */ + IF EXISTS (SELECT * FROM #UpdatedStats WHERE RowsForSorting > -1) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 44 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Statistics Updated Recently' AS Finding, + 'https://www.brentozar.com/go/stats' AS URL, + 'In the last 15 minutes, statistics were updated. To see which ones, click the HowToStopIt column.' + @LineFeed + @LineFeed + + 'This effectively clears the plan cache for queries that involve these tables,' + @LineFeed + + 'which thereby causes parameter sniffing: those queries are now getting brand new' + @LineFeed + + 'query plans based on whatever parameters happen to call them next.' + @LineFeed + @LineFeed + + 'Be on the lookout for sudden parameter sniffing issues after this time range.', + HowToStopIt = (SELECT (SELECT HowToStopIt + NCHAR(10)) + FROM #UpdatedStats + ORDER BY RowsForSorting DESC + FOR XML PATH('')); + + 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 + RAISERROR('Running CheckID 49',10,1) WITH NOWAIT; + END + IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%64%' AND SERVERPROPERTY('EngineEdition') <> 5 + BEGIN + IF @logical_processors <= 4 + SET @max_worker_threads = 512; + ELSE IF @logical_processors > 64 AND + ((@v = 13 AND @build >= 5026) OR @v >= 14) + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 32) + ELSE + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 16) + + IF @max_worker_threads > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 49 AS CheckID, + 210 AS Priority, + 'Potential Upcoming Problems' AS FindingGroup, + 'High Number of Connections' AS Finding, + 'https://www.brentozar.com/archive/2014/05/connections-slow-sql-server-threadpool/' AS URL, + 'There are ' + CAST(SUM(1) AS VARCHAR(20)) + ' open connections, which would lead to ' + @LineFeed + 'worker thread exhaustion and THREADPOOL waits' + @LineFeed + 'if they all ran queries at the same time.' AS Details + FROM sys.dm_exec_connections c + HAVING SUM(1) > @max_worker_threads; + 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; + + + /* End of checks. If we haven't waited @Seconds seconds, wait. */ + IF DATEADD(SECOND,1,SYSDATETIMEOFFSET()) < @FinishSampleTime + BEGIN + RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; + WAITFOR TIME @FinishSampleTimeWaitFor; + END; + + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END + + RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; + /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + @thread_time_ms AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + owt.wait_type, + SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + os.wait_type, + SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, + SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, + SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; + + SET @StringToExecute = @StringToExecute + N' + ) x + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 + ) + GROUP BY x.Pass, x.SampleTime, x.wait_type + ORDER BY sum_wait_time_ms DESC;'; + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 2 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 2 + OPTION(RECOMPILE); + + + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , + num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) + SELECT 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + mf.[database_id], + mf.[file_id], + DB_NAME(vfs.database_id) AS [db_name], + mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , + CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , + vfs.io_stall_read_ms , + vfs.num_of_reads , + vfs.[num_of_bytes_read], + vfs.io_stall_write_ms , + vfs.num_of_writes , + vfs.[num_of_bytes_written], + mf.physical_name, + mf.type_desc, + 0, + 0 + FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs + INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id + AND vfs.database_id = mf.database_id + WHERE vfs.num_of_reads > 0 + OR vfs.num_of_writes > 0; + + INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) + SELECT 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), dmv.cntr_value, dmv.cntr_type + FROM #PerfmonCounters counters + INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS + AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); + + /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ + UPDATE fNow + SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms + WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0; + + UPDATE fNow + SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms + WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0; + + UPDATE pNow + SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, + [value_per_second] = ((1.0 * pNow.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime)) + FROM #PerfmonStats pNow + INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pNow.[object_name] AND pFirst.counter_name = pNow.counter_name AND (pFirst.instance_name = pNow.instance_name OR (pFirst.instance_name IS NULL AND pNow.instance_name IS NULL)) + AND pNow.ID > pFirst.ID + WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; + + + /* Query Stats - If we're within 10 seconds of our projected finish time, do the plan cache analysis. - CheckID 18 */ + IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 18',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'https://www.brentozar.com/go/topqueries', + 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.'); + + END; + ELSE IF @CheckProcedureCache = 1 + BEGIN + + + RAISERROR('@CheckProcedureCache = 1, capturing second pass of plan cache',10,1) WITH NOWAIT; + + /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= @StartSampleTimeText + AND attr.attribute = ''dbid'';'; + END; + END; + ELSE + BEGIN + IF @FilterPlansByDatabase IS NULL + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText'; + END; + ELSE + BEGIN + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr + INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID + WHERE qs.last_execution_time >= @StartSampleTimeText + AND attr.attribute = ''dbid'';'; + END; + END; + /* Old version pre-2016/06/13: + IF @@VERSION LIKE 'Microsoft SQL Server 2005%' + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + ELSE + SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) + SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 + FROM sys.dm_exec_query_stats qs + WHERE qs.last_execution_time >= @StartSampleTimeText;'; + */ + SET @ParmDefinitions = N'@StartSampleTimeText NVARCHAR(100)'; + SET @Parm1 = CONVERT(NVARCHAR(100), CAST(@StartSampleTime AS DATETIME), 127); + + EXECUTE sp_executesql @StringToExecute, @ParmDefinitions, @StartSampleTimeText = @Parm1; + + RAISERROR('@CheckProcedureCache = 1, totaling up plan cache metrics',10,1) WITH NOWAIT; + + /* Get the totals for the entire plan cache */ + INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) + SELECT 0 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) + FROM sys.dm_exec_query_stats qs; + + + RAISERROR('@CheckProcedureCache = 1, so analyzing execution plans',10,1) WITH NOWAIT; + /* + Pick the most resource-intensive queries to review. Update the Points field + in #QueryStats - if a query is in the top 10 for logical reads, CPU time, + duration, or execution, add 1 to its points. + */ + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_elapsed_time > qsFirst.total_elapsed_time + AND qsNow.Pass = 2 + AND qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ + ORDER BY (qsNow.total_elapsed_time - COALESCE(qsFirst.total_elapsed_time, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; + + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_logical_reads > qsFirst.total_logical_reads + AND qsNow.Pass = 2 + AND qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ + ORDER BY (qsNow.total_logical_reads - COALESCE(qsFirst.total_logical_reads, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; + + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.total_worker_time > qsFirst.total_worker_time + AND qsNow.Pass = 2 + AND qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */ + ORDER BY (qsNow.total_worker_time - COALESCE(qsFirst.total_worker_time, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; + + WITH qsTop AS ( + SELECT TOP 10 qsNow.ID + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.execution_count > qsFirst.execution_count + AND qsNow.Pass = 2 + AND (qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ + OR qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ + OR qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */) + ORDER BY (qsNow.execution_count - COALESCE(qsFirst.execution_count, 0)) DESC) + UPDATE #QueryStats + SET Points = Points + 1 + FROM #QueryStats qs + INNER JOIN qsTop ON qs.ID = qsTop.ID; + + /* Query Stats - Most Resource-Intensive Queries - CheckID 17 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 17',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle, QueryHash) + SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'https://www.brentozar.com/go/topqueries', + 'Query stats during the sample:' + @LineFeed + + 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + + 'CPU Time: ' + CAST(qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Logical Reads: ' + CAST(qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)) AS NVARCHAR(100)) + @LineFeed + + 'Logical Writes: ' + CAST(qsNow.total_logical_writes - (COALESCE(qsFirst.total_logical_writes, 0)) AS NVARCHAR(100)) + @LineFeed + + 'CLR Time: ' + CAST(qsNow.total_clr_time - (COALESCE(qsFirst.total_clr_time, 0)) AS NVARCHAR(100)) + @LineFeed + + @LineFeed + @LineFeed + 'Query stats since ' + CONVERT(NVARCHAR(100), qsNow.creation_time ,121) + @LineFeed + + 'Executions: ' + CAST(qsNow.execution_count AS NVARCHAR(100)) + + CASE qsTotal.execution_count WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time AS NVARCHAR(100)) + + CASE qsTotal.total_elapsed_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'CPU Time: ' + CAST(qsNow.total_worker_time AS NVARCHAR(100)) + + CASE qsTotal.total_worker_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Logical Reads: ' + CAST(qsNow.total_logical_reads AS NVARCHAR(100)) + + CASE qsTotal.total_logical_reads WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'Logical Writes: ' + CAST(qsNow.total_logical_writes AS NVARCHAR(100)) + + CASE qsTotal.total_logical_writes WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_writes / qsTotal.total_logical_writes AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + 'CLR Time: ' + CAST(qsNow.total_clr_time AS NVARCHAR(100)) + + CASE qsTotal.total_clr_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_clr_time / qsTotal.total_clr_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + + --@LineFeed + @LineFeed + 'Query hash: ' + CAST(qsNow.query_hash AS NVARCHAR(100)) + @LineFeed + + --@LineFeed + @LineFeed + 'Query plan hash: ' + CAST(qsNow.query_plan_hash AS NVARCHAR(100)) + + @LineFeed AS Details, + 'See the URL for tuning tips on why this query may be consuming resources.' AS HowToStopIt, + qp.query_plan, + QueryText = SUBSTRING(st.text, + (qsNow.statement_start_offset / 2) + 1, + ((CASE qsNow.statement_end_offset + WHEN -1 THEN DATALENGTH(st.text) + ELSE qsNow.statement_end_offset + END - qsNow.statement_start_offset) / 2) + 1), + qsNow.ID AS QueryStatsNowID, + qsFirst.ID AS QueryStatsFirstID, + qsNow.plan_handle AS PlanHandle, + qsNow.query_hash + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st + CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp + WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL; + + UPDATE #BlitzFirstResults + SET DatabaseID = CAST(attr.value AS INT), + DatabaseName = DB_NAME(CAST(attr.value AS INT)) + FROM #BlitzFirstResults + CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr + WHERE attr.attribute = 'dbid'; + + + END; /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ + + + RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; + + /* Wait Stats - CheckID 6 */ + /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 6',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) + SELECT TOP 10 6 AS CheckID, + 200 AS Priority, + 'Wait Stats' AS FindingGroup, + wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ + N'https://www.sqlskills.com/help/waits/' + LOWER(wNow.wait_type) + '/' AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt + FROM #WaitStats wNow + LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime + WHERE wNow.wait_time_ms > (wBase.wait_time_ms + (.5 * (DATEDIFF(ss,@StartSampleTime,@FinishSampleTime)) * 1000)) /* Only look for things we've actually waited on for half of the time or more */ + ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; + + /* Server Performance - Poison Wait Detected - CheckID 30 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 30',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) + SELECT 30 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Poison Wait Detected: ' + wNow.wait_type AS Finding, + N'https://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt + FROM #WaitStats wNow + LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime + WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'POOL_LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') + AND wNow.wait_time_ms > (wBase.wait_time_ms + 1000); + + + /* Server Performance - Slow Data File Reads - CheckID 11 */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 11',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) + SELECT TOP 10 11 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Slow Data File Reads' AS Finding, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, + 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed + + 'File: ' + fNow.PhysicalName + @LineFeed + + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed + + 'Seconds spent waiting on storage for these reads: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed + + 'Average read latency during the sample: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed + + 'Microsoft guidance for data file read speed: 20ms or less.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + fNow.DatabaseID, + fNow.DatabaseName + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > (fBase.io_stall_read_ms + 1000) + WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS + AND fNow.TypeDesc = 'ROWS' + ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; + END; + + /* Server Performance - Slow Log File Writes - CheckID 12 */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 12',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) + SELECT TOP 10 12 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Slow Log File Writes' AS Finding, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, + 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed + + 'File: ' + fNow.PhysicalName + @LineFeed + + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed + + 'Seconds spent waiting on storage for these writes: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed + + 'Average write latency during the sample: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed + + 'Microsoft guidance for log file write speed: 3ms or less.' + @LineFeed + @LineFeed AS Details, + 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, + fNow.DatabaseID, + fNow.DatabaseName + FROM #FileStats fNow + INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > (fBase.io_stall_write_ms + 1000) + WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS + AND fNow.TypeDesc = 'LOG' + ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; + 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 + RAISERROR('Running CheckID 13',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 13 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Log File Growing' AS Finding, + 'https://www.brentozar.com/askbrent/file-growing/' AS URL, + 'Number of growths 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, + 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND object_name = @ServiceName + ':Databases' + AND counter_name = 'Log Growths' + AND value_delta > 0; + + + /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 14',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 14 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Log File Shrinking' AS Finding, + 'https://www.brentozar.com/askbrent/file-shrinking/' AS URL, + 'Number of shrinks 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, + 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + WHERE ps.Pass = 2 + AND object_name = @ServiceName + ':Databases' + AND counter_name = 'Log Shrinks' + AND value_delta > 0; + + /* Query Problems - Compilations/Sec High - CheckID 15 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 15',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 15 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Compilations/Sec High' AS Finding, + 'https://www.brentozar.com/askbrent/compilations/' AS URL, + 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, + 'To find the queries that are compiling, start with:' + @LineFeed + + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed + + 'If dynamic SQL or non-parameterized strings are involved, consider enabling Forced Parameterization. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Compilations/sec' AND psComp.value_delta > 0 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec' + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 compilations per second, or more compilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Compilations are more than 10% of batch requests per second */ + + /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 16',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 16 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Re-Compilations/Sec High' AS Finding, + 'https://www.brentozar.com/askbrent/recompilations/' AS URL, + 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, + 'To find the queries that are being forced to recompile, start with:' + @LineFeed + + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed + + 'Examine those plans to find out which objects are changing so quickly that they hit the stats update threshold. See the URL for more details.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Re-Compilations/sec' AND psComp.value_delta > 0 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec' + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 recompilations per second, or more recompilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Recompilations are more than 10% of batch requests per second */ + + /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 29',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 29 AS CheckID, + 40 AS Priority, + 'Table Problems' AS FindingGroup, + 'Forwarded Fetches/Sec High' AS Finding, + 'https://www.brentozar.com/go/fetch/' AS URL, + CAST(ps.value_delta AS NVARCHAR(20)) + ' forwarded fetches (from SQLServer:Access Methods counter)' + @LineFeed + + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, + 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Access Methods' AND psComp.counter_name = 'Forwarded Records/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':Access Methods' + AND ps.counter_name = 'Forwarded Records/sec' + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ + + /* Check for temp objects with high forwarded fetches. + This has to be done as dynamic SQL because we have to execute OBJECT_NAME inside TempDB. */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 29) + BEGIN + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT TOP 10 29 AS CheckID, + 40 AS Priority, + ''Table Problems'' AS FindingGroup, + ''Forwarded Fetches/Sec High: TempDB Object'' AS Finding, + ''https://www.brentozar.com/go/fetch/'' AS URL, + CAST(COALESCE(os.forwarded_fetch_count,0) - COALESCE(os_prior.forwarded_fetch_count,0) AS NVARCHAR(20)) + '' forwarded fetches on '' + + CASE WHEN OBJECT_NAME(os.object_id) IS NULL THEN ''an unknown table '' + WHEN LEN(OBJECT_NAME(os.object_id)) < 50 THEN ''a table variable, internal identifier '' + OBJECT_NAME(os.object_id) + ELSE ''a temp table '' + OBJECT_NAME(os.object_id) + END AS Details, + ''Look through your source code to find the object creating these objects, and tune the creation and population to reduce fetches. See the URL for details.'' AS HowToStopIt + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID(''tempdb''), NULL, NULL, NULL) os + LEFT OUTER JOIN #TempdbOperationalStats os_prior ON os.object_id = os_prior.object_id + AND os.forwarded_fetch_count > os_prior.forwarded_fetch_count + WHERE os.database_id = DB_ID(''tempdb'') + AND os.forwarded_fetch_count - COALESCE(os_prior.forwarded_fetch_count,0) > 100 + ORDER BY os.forwarded_fetch_count DESC;' + + EXECUTE sp_executesql @StringToExecute; + END + + /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 31',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 31 AS CheckID, + 50 AS Priority, + 'In-Memory OLTP' AS FindingGroup, + 'Garbage Collection in Progress' AS Finding, + 'https://www.brentozar.com/go/garbage/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed + + 'This can happen for a few reasons: ' + @LineFeed + + 'Memory-Optimized TempDB, or ' + @LineFeed + + 'transactional workloads that constantly insert/delete data in In-Memory OLTP tables, or ' + @LineFeed + + 'memory pressure (causing In-Memory OLTP to shrink its footprint) or' AS Details, + 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name LIKE '%XTP Garbage Collection' + AND ps.counter_name = 'Rows processed/sec' + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ + + /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 32',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 32 AS CheckID, + 100 AS Priority, + 'In-Memory OLTP' AS FindingGroup, + 'Transactions Aborted' AS Finding, + 'https://www.brentozar.com/go/aborted/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed + + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, + 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Transactions' AND psComp.counter_name = 'Transactions aborted/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name LIKE '%XTP Transactions' + AND ps.counter_name = 'Transactions aborted/sec' + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ + + /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 33',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 32 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Suboptimal Plans/Sec High' AS Finding, + 'https://www.brentozar.com/go/suboptimal/' AS URL, + CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed + + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, + 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Workload GroupStats' AND psComp.counter_name = 'Suboptimal plans/sec' AND psComp.value_delta > 100 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':Workload GroupStats' + AND ps.counter_name = 'Suboptimal plans/sec' + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ + + /* Azure Performance - Database is Maxed Out - CheckID 41 */ + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 41',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 41 AS CheckID, + 10 AS Priority, + 'Azure Performance' AS FindingGroup, + 'Database is Maxed Out' AS Finding, + 'https://www.brentozar.com/go/maxedout' AS URL, + N'At ' + CONVERT(NVARCHAR(100), s.end_time ,121) + N', your database approached (or hit) your DTU limits:' + @LineFeed + + N'Average CPU percent: ' + CAST(avg_cpu_percent AS NVARCHAR(50)) + @LineFeed + + N'Average data IO percent: ' + CAST(avg_data_io_percent AS NVARCHAR(50)) + @LineFeed + + N'Average log write percent: ' + CAST(avg_log_write_percent AS NVARCHAR(50)) + @LineFeed + + N'Max worker percent: ' + CAST(max_worker_percent AS NVARCHAR(50)) + @LineFeed + + N'Max session percent: ' + CAST(max_session_percent AS NVARCHAR(50)) AS Details, + 'Tune your queries or indexes with sp_BlitzCache or sp_BlitzIndex, or consider upgrading to a higher DTU level.' AS HowToStopIt + FROM sys.dm_db_resource_stats s + WHERE s.end_time >= DATEADD(MI, -5, GETDATE()) + AND (avg_cpu_percent > 90 + OR avg_data_io_percent >= 90 + OR avg_log_write_percent >=90 + OR max_worker_percent >= 90 + 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 + RAISERROR('Running CheckID 19',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 19 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'Batch Requests per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(CAST(ps.value_delta AS MONEY) / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'Batch Requests/sec'; + + + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); + + /* Server Info - SQL Compilations/sec - CheckID 25 */ + IF @ExpertMode >= 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 25',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 25 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Compilations/sec'; + END + + /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ + IF @ExpertMode >= 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 26',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 26 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Re-Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Re-Compilations/sec'; + END + + /* Server Info - Wait Time per Core per Sec - CheckID 20 */ + IF @Seconds > 0 + BEGIN; + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 20',10,1) WITH NOWAIT; + END; + + WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), + waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), + cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 20 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'Wait Time per Core per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST((CAST(waits2.waits_ms - waits1.waits_ms AS MONEY)) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS NVARCHAR(20)) AS Details, + (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS DetailsInt + FROM cores i + CROSS JOIN waits1 + CROSS JOIN waits2; + END; + + IF @Seconds > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 2 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END + + /* If we're waiting 30+ seconds, run these checks at the end. + We get this data from the ring buffers, and it's only updated once per minute, so might + as well get it now - whereas if we're checking 30+ seconds, it might get updated by the + end of our sp_BlitzFirst session. */ + IF @Seconds >= 30 + BEGIN + /* Server Performance - High CPU Utilization CheckID 24 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; + 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' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; + + /* Server Performance - CPU Utilization CheckID 23 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + 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' + FROM ( + SELECT record, + record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle + 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; + + END; /* IF @Seconds >= 30 */ + + IF /* Let people on <2016 know about the thread time column */ + ( + @Seconds > 0 + AND @total_cpu_usage = 0 + ) + BEGIN + INSERT INTO + #BlitzFirstResults + ( + CheckID, + Priority, + FindingsGroup, + Finding, + Details, + URL + ) + SELECT + 48, + 254, + N'Informational', + N'Thread Time comes from the plan cache in versions earlier than 2016, and is not as reliable', + N'The oldest plan in your cache is from ' + + CONVERT(nvarchar(30), MIN(s.creation_time)) + + N' and your server was last restarted on ' + + CONVERT(nvarchar(30), MAX(o.sqlserver_start_time)), + N'https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-schedulers-transact-sql' + FROM sys.dm_exec_query_stats AS s + CROSS JOIN sys.dm_os_sys_info AS o + OPTION(RECOMPILE); + END /* Let people on <2016 know about the thread time column */ + + /* If we didn't find anything, apologize. */ + IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) + BEGIN + + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 1 , + 'No Problems Found' , + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' + ); + + END; /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ + + /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + VALUES ( -1 , + 255 , + 'Thanks!' , + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' + ); + + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + + ) + VALUES ( -1 , + 0 , + 'sp_BlitzFirst ' + CAST(CONVERT(DATETIMEOFFSET, @VersionDate, 102) AS VARCHAR(100)), + 'From Your Community Volunteers' , + 'http://FirstResponderKit.org/' , + 'We hope you found this tool useful.' + ); + + /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old - CheckID 27 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 27',10,1) WITH NOWAIT; + END + + IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 + BEGIN + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 27 AS CheckID , + 0 AS Priority , + 'Outdated sp_BlitzFirst' AS FindingsGroup , + 'sp_BlitzFirst is Over 6 Months Old' AS Finding , + 'http://FirstResponderKit.org/' AS URL , + 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details; + END; + + IF @CheckServerInfo = 0 /* Github #1680 */ + BEGIN + DELETE #BlitzFirstResults + WHERE FindingsGroup = 'Server Info'; + END + + RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; + + + /* If they want to run sp_BlitzCache and export to table, go for it. */ + IF @OutputTableNameBlitzCache IS NOT NULL + AND @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + + + RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; + + + /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ + IF EXISTS (SELECT * FROM sys.objects o + INNER JOIN sys.parameters pMB ON o.object_id = pMB.object_id AND pMB.name = '@MinutesBack' + INNER JOIN sys.parameters pCDO ON o.object_id = pCDO.object_id AND pCDO.name = '@CheckDateOverride' + WHERE o.name = 'sp_BlitzCache') + BEGIN + /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + QUOTENAME(@OutputTableNameBlitzCache) + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + QUOTENAME(@OutputTableNameBlitzCache) + + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; + EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; + + /* If there's no data, let's just analyze the last 15 minutes of the plan cache */ + IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 + SET @BlitzCacheMinutesBack = 15; + + IF(@OutputType = 'NONE') + BEGIN + EXEC sp_BlitzCache + @OutputDatabaseName = @UnquotedOutputDatabaseName, + @OutputSchemaName = @UnquotedOutputSchemaName, + @OutputTableName = @OutputTableNameBlitzCache, + @CheckDateOverride = @StartSampleTime, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, + @MinutesBack = @BlitzCacheMinutesBack, + @Debug = @Debug, + @OutputType = @OutputType + ; + END; + ELSE + BEGIN + + EXEC sp_BlitzCache + @OutputDatabaseName = @UnquotedOutputDatabaseName, + @OutputSchemaName = @UnquotedOutputSchemaName, + @OutputTableName = @OutputTableNameBlitzCache, + @CheckDateOverride = @StartSampleTime, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, + @MinutesBack = @BlitzCacheMinutesBack, + @Debug = @Debug + ; + END; + + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + QUOTENAME(@OutputTableNameBlitzCache) + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + + END; + + ELSE + BEGIN + /* No sp_BlitzCache found, or it's outdated - CheckID 36 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 36',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 36 AS CheckID , + 0 AS Priority , + 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , + 'Update Your sp_BlitzCache' AS Finding , + 'http://FirstResponderKit.org/' AS URL , + 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details; + END; + + RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; + + END; /* End running sp_BlitzCache */ + + /* @OutputTableName lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND @OutputTableName NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NOT NULL, + Details NVARCHAR(4000) NULL, + HowToStopIt [XML] NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; + + EXEC(@StringToExecute); + + /* If the table doesn't have the new QueryHash column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''QueryHash'') + ALTER TABLE ' + @ObjectFullName + N' ADD QueryHash BINARY(8) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2164. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); + + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NULL) CREATE TABLE ' + + @OutputTableName + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + CheckID INT NOT NULL, + Priority TINYINT NOT NULL, + FindingsGroup VARCHAR(50) NOT NULL, + Finding VARCHAR(200) NOT NULL, + URL VARCHAR(200) NOT NULL, + Details NVARCHAR(4000) NULL, + HowToStopIt [XML] NULL, + QueryPlan [XML] NULL, + QueryText NVARCHAR(MAX) NULL, + StartTime DATETIMEOFFSET NULL, + LoginName NVARCHAR(128) NULL, + NTUserName NVARCHAR(128) NULL, + OriginalLoginName NVARCHAR(128) NULL, + ProgramName NVARCHAR(128) NULL, + HostName NVARCHAR(128) NULL, + DatabaseID INT NULL, + DatabaseName NVARCHAR(128) NULL, + OpenTransactionCount INT NULL, + DetailsInt INT NULL, + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + + /* @OutputTableNameFileStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNameFileStats IS NOT NULL + AND @OutputTableNameFileStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNameFileStats + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + PRIMARY KEY CLUSTERED (ID ASC));'; + + EXEC(@StringToExecute); + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View + N';'; + + EXEC(@StringToExecute); + END + + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + ' SELECT f.ServerName,' + @LineFeed + + ' f.CheckDate,' + @LineFeed + + ' f.DatabaseID,' + @LineFeed + + ' f.DatabaseName,' + @LineFeed + + ' f.FileID,' + @LineFeed + + ' f.FileLogicalName,' + @LineFeed + + ' f.TypeDesc,' + @LineFeed + + ' f.PhysicalName,' + @LineFeed + + ' f.SizeOnDiskMB,' + @LineFeed + + ' DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds,' + @LineFeed + + ' (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth,' + @LineFeed + + ' (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms,' + @LineFeed + + ' io_stall_read_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_reads - fPrior.num_of_reads) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads,' + @LineFeed + + ' (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read,' + @LineFeed + + ' (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms,' + @LineFeed + + ' io_stall_write_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_writes - fPrior.num_of_writes) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes,' + @LineFeed + + ' (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written, ' + @LineFeed + + ' f.ServerName + CAST(f.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed + + ' INNER HASH JOIN CheckDates DATES ON f.CheckDate = DATES.CheckDate' + @LineFeed + + ' INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName' + @LineFeed + + ' AND f.DatabaseID = fPrior.DatabaseID' + @LineFeed + + ' AND f.FileID = fPrior.FileID' + @LineFeed + + ' AND fPrior.CheckDate = DATES.PreviousCheckDate' + @LineFeed + + '' + @LineFeed + + ' WHERE f.num_of_reads >= fPrior.num_of_reads' + @LineFeed + + ' AND f.num_of_writes >= fPrior.num_of_writes' + @LineFeed + + ' AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END; + + + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameFileStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + END; + ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNameFileStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNameFileStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + DatabaseID INT NOT NULL, + FileID INT NOT NULL, + DatabaseName NVARCHAR(256) , + FileLogicalName NVARCHAR(256) , + TypeDesc NVARCHAR(60) , + SizeOnDiskMB BIGINT , + io_stall_read_ms BIGINT , + num_of_reads BIGINT , + bytes_read BIGINT , + io_stall_write_ms BIGINT , + num_of_writes BIGINT , + bytes_written BIGINT, + PhysicalName NVARCHAR(520) , + DetailsInt INT NULL, + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNameFileStats + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + + + /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNamePerfmonStats IS NOT NULL + AND @OutputTableNamePerfmonStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNamePerfmonStats + ''') CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL, + PRIMARY KEY CLUSTERED (ID ASC));'; + + EXEC(@StringToExecute); + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View + N';'; + + EXEC(@StringToExecute); + END + + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + 'SELECT' + @LineFeed + + ' pMon.[ServerName]' + @LineFeed + + ' ,pMon.[CheckDate]' + @LineFeed + + ' ,pMon.[object_name]' + @LineFeed + + ' ,pMon.[counter_name]' + @LineFeed + + ' ,pMon.[instance_name]' + @LineFeed + + ' ,DATEDIFF(SECOND,pMonPrior.[CheckDate],pMon.[CheckDate]) AS ElapsedSeconds' + @LineFeed + + ' ,pMon.[cntr_value]' + @LineFeed + + ' ,pMon.[cntr_type]' + @LineFeed + + ' ,(pMon.[cntr_value] - pMonPrior.[cntr_value]) AS cntr_delta' + @LineFeed + + ' ,(pMon.cntr_value - pMonPrior.cntr_value) * 1.0 / DATEDIFF(ss, pMonPrior.CheckDate, pMon.CheckDate) AS cntr_delta_per_second' + @LineFeed + + ' ,pMon.ServerName + CAST(pMon.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMon' + @LineFeed + + ' INNER HASH JOIN CheckDates Dates' + @LineFeed + + ' ON Dates.CheckDate = pMon.CheckDate' + @LineFeed + + ' JOIN ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMonPrior' + @LineFeed + + ' ON Dates.PreviousCheckDate = pMonPrior.CheckDate' + @LineFeed + + ' AND pMon.[ServerName] = pMonPrior.[ServerName] ' + @LineFeed + + ' AND pMon.[object_name] = pMonPrior.[object_name] ' + @LineFeed + + ' AND pMon.[counter_name] = pMonPrior.[counter_name] ' + @LineFeed + + ' AND pMon.[instance_name] = pMonPrior.[instance_name]' + @LineFeed + + ' WHERE DATEDIFF(MI, pMonPrior.CheckDate, pMon.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View + N';'; + + EXEC(@StringToExecute); + END + + /* Create the second view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStatsActuals_View + ' AS ' + @LineFeed + + 'WITH PERF_AVERAGE_BULK AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CASE WHEN CHARINDEX(''''('''', counter_name) = 0 THEN counter_name ELSE LEFT (counter_name, CHARINDEX(''''('''',counter_name)-1) END AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + @LineFeed + + ' WHERE cntr_type IN(1073874176)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_LARGE_RAW_BASE AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' LEFT(counter_name, CHARINDEX(''''BASE'''', UPPER(counter_name))-1) AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(1073939712)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_AVERAGE_FRACTION AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' counter_name AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(537003264)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_BULK_COUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta / ElapsedSeconds AS cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(272696576, 272696320)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_RAWCOUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(65792, 65536)' + @LineFeed + + ')' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' NUM.cntr_delta / DEN.cntr_delta AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' ' + @LineFeed + + 'FROM PERF_AVERAGE_BULK AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' CAST((CAST(NUM.cntr_delta as DECIMAL(19)) / DEN.cntr_delta) as decimal(23,3)) AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_AVERAGE_FRACTION AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_BULK_COUNT' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_RAWCOUNT;'')'; + + EXEC(@StringToExecute); + END; + + + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + + + END; + ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNamePerfmonStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNamePerfmonStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + [object_name] NVARCHAR(128) NOT NULL, + [counter_name] NVARCHAR(128) NOT NULL, + [instance_name] NVARCHAR(128) NULL, + [cntr_value] BIGINT NULL, + [cntr_type] INT NOT NULL, + [value_delta] BIGINT NULL, + [value_per_second] DECIMAL(18,2) NULL, + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNamePerfmonStats + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + + + /* @OutputTableNameWaitStats lets us export the results to a permanent table */ + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableNameWaitStats IS NOT NULL + AND @OutputTableNameWaitStats NOT LIKE '#%' + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + /* Create the table */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + ''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableNameWaitStats + ''') ' + @LineFeed + + 'BEGIN' + @LineFeed + + 'CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT , + PRIMARY KEY CLUSTERED (ID));' + @LineFeed + + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed + + 'END'; + + EXEC(@StringToExecute); + + /* Create the wait stats category table */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_Categories; + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE TABLE ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')'; + + EXEC(@StringToExecute); + END; + + /* Make sure the wait stats category table has the current number of rows */ + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''IF (SELECT COALESCE(SUM(1),0) FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ') <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories)' + @LineFeed + + 'BEGIN ' + @LineFeed + + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed + + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed + + 'END'')'; + + EXEC(@StringToExecute); + + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View + N';'; + + EXEC(@StringToExecute); + END + + + /* Create the wait stats view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed + + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) / 60000.0 AS wait_time_minutes_delta' + @LineFeed + + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed + + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed + + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed + + ', w.ServerName + CAST(w.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed + + 'INNER HASH JOIN CheckDates Dates' + @LineFeed + + 'ON Dates.CheckDate = w.CheckDate' + @LineFeed + + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND Dates.PreviousCheckDate = wPrior.CheckDate' + @LineFeed + + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed + + 'WHERE DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60' + @LineFeed + + 'AND [w].[wait_time_ms] >= [wPrior].[wait_time_ms];'')' + + EXEC(@StringToExecute); + END; + + + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + + /* Delete history older than @OutputTableRetentionDays */ + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableNameWaitStats + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + END; + ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableNameWaitStats + + ''') IS NULL) CREATE TABLE ' + + @OutputTableNameWaitStats + + ' (ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128), + CheckDate DATETIMEOFFSET, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT , + PRIMARY KEY CLUSTERED (ID ASC));' + + ' INSERT ' + + @OutputTableNameWaitStats + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; + ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + + + + + DECLARE @separator AS VARCHAR(1); + IF @OutputType = 'RSV' + SET @separator = CHAR(31); + ELSE + SET @separator = ','; + + IF @OutputType = 'COUNT' AND @SinceStartup = 0 + BEGIN + SELECT COUNT(*) AS Warnings + FROM #BlitzFirstResults; + END; + ELSE + IF @OutputType = 'Opserver1' AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + + SELECT r.[Priority] , + r.[FindingsGroup] , + r.[Finding] , + r.[URL] , + r.[Details], + r.[HowToStopIt] , + r.[CheckID] , + r.[StartTime], + r.[LoginName], + r.[NTUserName], + r.[OriginalLoginName], + r.[ProgramName], + r.[HostName], + r.[DatabaseID], + r.[DatabaseName], + r.[OpenTransactionCount], + r.[QueryPlan], + r.[QueryText], + qsNow.plan_handle AS PlanHandle, + qsNow.sql_handle AS SqlHandle, + qsNow.statement_start_offset AS StatementStartOffset, + qsNow.statement_end_offset AS StatementEndOffset, + [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), + [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), + [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), + [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), + [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), + [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), + [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), + [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), + [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), + [TotalExecutions] = qsNow.execution_count, + [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), + [TotalDuration] = qsNow.total_elapsed_time, + [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), + [TotalCPU] = qsNow.total_worker_time, + [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), + [TotalReads] = qsNow.total_logical_reads, + [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), + r.[DetailsInt] + FROM #BlitzFirstResults r + LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 + LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID + LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID + ORDER BY r.Priority , + r.FindingsGroup , + CASE + WHEN r.CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + r.Finding, + r.ID; + END; + ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + + SELECT Result = CAST([Priority] AS NVARCHAR(100)) + + @separator + CAST(CheckID AS NVARCHAR(100)) + + @separator + COALESCE([FindingsGroup], + '(N/A)') + @separator + + COALESCE([Finding], '(N/A)') + @separator + + COALESCE(DatabaseName, '(N/A)') + @separator + + COALESCE([URL], '(N/A)') + @separator + + COALESCE([Details], '(N/A)') + FROM #BlitzFirstResults + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + Details; + END; + ELSE IF @OutputType = 'Top10' AND @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in hours */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT TOP 10 + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], + 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], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] + 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + ELSE IF @ExpertMode = 0 AND @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS XML) AS Details, + CAST(@StockWarningHeader + HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, + [QueryText], + [QueryPlan] + FROM #BlitzFirstResults + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' + BEGIN + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + CAST(LEFT(@StockDetailsHeader + [Details] + @StockDetailsFooter,32000) AS TEXT) AS Details, + CAST(LEFT([HowToStopIt],32000) AS TEXT) AS HowToStopIt, + CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, + CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan + FROM #BlitzFirstResults + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY Priority , + FindingsGroup , + CASE + WHEN CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + Finding, + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @ExpertMode >= 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%Findings%' + BEGIN + IF @SinceStartup = 0 + SELECT r.[Priority] , + r.[FindingsGroup] , + r.[Finding] , + r.[URL] , + CAST(@StockDetailsHeader + r.[Details] + @StockDetailsFooter AS XML) AS Details, + CAST(@StockWarningHeader + r.HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, + r.[CheckID] , + r.[StartTime], + r.[LoginName], + r.[NTUserName], + r.[OriginalLoginName], + r.[ProgramName], + r.[HostName], + r.[DatabaseID], + r.[DatabaseName], + r.[OpenTransactionCount], + r.[QueryPlan], + r.[QueryText], + qsNow.plan_handle AS PlanHandle, + qsNow.sql_handle AS SqlHandle, + qsNow.statement_start_offset AS StatementStartOffset, + qsNow.statement_end_offset AS StatementEndOffset, + [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), + [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), + [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), + [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), + [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), + [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), + [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), + [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), + [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), + [TotalExecutions] = qsNow.execution_count, + [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), + [TotalDuration] = qsNow.total_elapsed_time, + [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), + [TotalCPU] = qsNow.total_worker_time, + [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), + [TotalReads] = qsNow.total_logical_reads, + [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), + r.[DetailsInt] + FROM #BlitzFirstResults r + LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 + LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 + LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID + LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID + WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ + ORDER BY r.Priority , + r.FindingsGroup , + CASE + WHEN r.CheckID = 6 THEN DetailsInt + ELSE 0 + END DESC, + r.Finding, + r.ID, + CAST(r.Details AS NVARCHAR(4000)); + + ------------------------- + --What happened: #WaitStats + ------------------------- + IF @Seconds = 0 AND @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in hours */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT + 'WAIT STATS' AS Pattern, + b.SampleTime AS [Sample Ended], + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60. AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + ELSE IF @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in seconds */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT + 'WAIT STATS' AS Pattern, + b.SampleTime AS [Sample Ended], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], + c.[Total Thread Time (Seconds)], + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms - wd1.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + + ------------------------- + --What happened: #FileStats + ------------------------- + IF @OutputResultSets LIKE N'%FileStats%' + WITH readstats AS ( + SELECT 'PHYSICAL READS' AS Pattern, + ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, + wd2.SampleTime AS [Sample Time], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], + wd1.DatabaseName , + wd1.FileLogicalName AS [File Name], + UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , + wd1.SizeOnDiskMB , + ( wd2.num_of_reads - wd1.num_of_reads ) AS [# Reads/Writes], + CASE WHEN wd2.num_of_reads - wd1.num_of_reads > 0 + THEN CAST(( wd2.bytes_read - wd1.bytes_read)/1024./1024. AS NUMERIC(21,1)) + ELSE 0 + END AS [MB Read/Written], + wd2.avg_stall_read_ms AS [Avg Stall (ms)], + wd1.PhysicalName AS [file physical name] + FROM #FileStats wd2 + JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime + AND wd1.DatabaseID = wd2.DatabaseID + AND wd1.FileID = wd2.FileID + ), + writestats AS ( + SELECT + 'PHYSICAL WRITES' AS Pattern, + ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_write_ms DESC) AS StallRank, + wd2.SampleTime AS [Sample Time], + DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], + wd1.DatabaseName , + wd1.FileLogicalName AS [File Name], + UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , + wd1.SizeOnDiskMB , + ( wd2.num_of_writes - wd1.num_of_writes ) AS [# Reads/Writes], + CASE WHEN wd2.num_of_writes - wd1.num_of_writes > 0 + THEN CAST(( wd2.bytes_written - wd1.bytes_written)/1024./1024. AS NUMERIC(21,1)) + ELSE 0 + END AS [MB Read/Written], + wd2.avg_stall_write_ms AS [Avg Stall (ms)], + wd1.PhysicalName AS [file physical name] + FROM #FileStats wd2 + JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime + AND wd1.DatabaseID = wd2.DatabaseID + AND wd1.FileID = wd2.FileID + ) + SELECT + Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] + FROM readstats + WHERE StallRank <=20 AND [MB Read/Written] > 0 + UNION ALL + SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] + FROM writestats + WHERE StallRank <=20 AND [MB Read/Written] > 0 + ORDER BY Pattern, StallRank; + + + ------------------------- + --What happened: #PerfmonStats + ------------------------- + + IF @OutputResultSets LIKE N'%PerfmonStats%' + SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, + pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, + pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, + pLast.cntr_value - pFirst.cntr_value AS ValueDelta, + ((1.0 * pLast.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pLast.SampleTime)) AS ValuePerSecond + FROM #PerfmonStats pLast + INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) + AND pLast.ID > pFirst.ID + WHERE pLast.cntr_value <> pFirst.cntr_value + ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name; + + + ------------------------- + --What happened: #QueryStats + ------------------------- + IF @CheckProcedureCache = 1 AND @OutputResultSets LIKE N'%BlitzCache%' + BEGIN + + SELECT qsNow.*, qsFirst.* + FROM #QueryStats qsNow + INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 + WHERE qsNow.Pass = 2; + END; + ELSE IF @OutputResultSets LIKE N'%BlitzCache%' + BEGIN + SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details]; + END; + END; + + DROP TABLE #BlitzFirstResults; + + /* What's running right now? This is the first and last result set. */ + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_End%' + BEGIN + IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL + BEGIN + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; + ELSE + BEGIN + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ + +END; /* IF @LogMessage IS NULL */ +END; /* ELSE IF @OutputType = 'SCHEMA' */ + +SET NOCOUNT OFF; +GO + + + +/* How to run it: +EXEC dbo.sp_BlitzFirst + +With extra diagnostic info: +EXEC dbo.sp_BlitzFirst @ExpertMode = 1; + +Saving output to tables: +EXEC sp_BlitzFirst + @OutputDatabaseName = 'DBAtools' +, @OutputSchemaName = 'dbo' +, @OutputTableName = 'BlitzFirst' +, @OutputTableNameFileStats = 'BlitzFirst_FileStats' +, @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' +, @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' +, @OutputTableNameBlitzCache = 'BlitzCache' +, @OutputTableNameBlitzWho = 'BlitzWho' +, @OutputType = 'none' +*/ +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 STATISTICS IO OFF; +SET STATISTICS TIME OFF; +GO + +IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzIndex AS RETURN 0;'); +GO + +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.*/ + @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 */ + /*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, + @IgnoreDatabases NVARCHAR(MAX) = NULL, /* Comma-delimited list of databases you want to skip */ + @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(256) = NULL , + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = 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 */, + @SortOrder NVARCHAR(50) = NULL, /* Only affects @Mode = 2. */ + @SortDirection NVARCHAR(4) = 'DESC', /* Only affects @Mode = 2. */ + @Help TINYINT = 0, + @Debug BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 +WITH RECOMPILE +AS +SET NOCOUNT ON; +SET STATISTICS XML OFF; +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +IF @Help = 1 +BEGIN +PRINT ' +/* +sp_BlitzIndex from http://FirstResponderKit.org + +This script analyzes the design and performance of your indexes. + +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. + +Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. + -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important + for the user to understand if it is going to be offline and not just run a script. + -- Example 2: they do not include all the options the index may have been created with (padding, compression + filegroup/partition scheme etc.) + -- (The compression and filegroup index create syntax is not trivial because it is set at the partition + level and is not trivial to code.) + - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of problems.) + +Unknown limitations of this version: + - We knew them once, but we forgot. + + +MIT License + +Copyright (c) Brent Ozar Unlimited + +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. +'; +RETURN; +END; /* @Help = 1 */ + +DECLARE @ScriptVersionName NVARCHAR(50); +DECLARE @DaysUptime NUMERIC(23,2); +DECLARE @DatabaseID INT; +DECLARE @ObjectID INT; +DECLARE @dsql NVARCHAR(MAX); +DECLARE @params NVARCHAR(MAX); +DECLARE @msg NVARCHAR(4000); +DECLARE @ErrorSeverity INT; +DECLARE @ErrorState INT; +DECLARE @Rowcount BIGINT; +DECLARE @SQLServerProductVersion NVARCHAR(128); +DECLARE @SQLServerEdition INT; +DECLARE @FilterMB INT; +DECLARE @collation NVARCHAR(256); +DECLARE @NumDatabases INT; +DECLARE @LineFeed NVARCHAR(5); +DECLARE @DaysUptimeInsertValue NVARCHAR(256); +DECLARE @DatabaseToIgnore NVARCHAR(MAX); +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 */ +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 */ + + +/* Let's get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); +SET @SortDirection = LOWER(@SortDirection); + +SET @LineFeed = CHAR(13) + CHAR(10); +SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); +SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ +SET @FilterMB=250; +SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate); +SET @IgnoreDatabases = REPLACE(REPLACE(LTRIM(RTRIM(@IgnoreDatabases)), CHAR(10), ''), CHAR(13), ''); + +SELECT + @OptimizeForSequentialKey = + CASE WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.indexes') + AND ac.name = N'optimize_for_sequential_key' + ) + THEN 1 + ELSE 0 + END; + +RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; + + +IF(@OutputType NOT IN ('TABLE','NONE')) +BEGIN + RAISERROR('Invalid value for parameter @OutputType. Expected: (TABLE;NONE)',12,1); + 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; + SET @OutputType = 'NONE' +END; + +IF(@OutputType = 'NONE') +BEGIN + + IF ((@OutputServerName IS NOT NULL) AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) + BEGIN + RAISERROR('Parameter @OutputServerName is specified, rest of @Output* parameters needs to also be specified',12,1); + RETURN; + END; + + IF(@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL) + BEGIN + RAISERROR('This procedure should be called with a value for @OutputTableName, @OutputSchemaName and @OutputDatabaseName parameters, as @OutputType is set to NONE',12,1); + RETURN; + END; + /* Output is supported for all modes, no reason to not bring pain and output + IF(@BringThePain = 1) + BEGIN + RAISERROR('Incompatible Parameters: @BringThePain set to 1 and @OutputType set to NONE',12,1); + RETURN; + END; + */ + /* Eventually limit by mode + IF(@Mode not in (0,4)) + BEGIN + RAISERROR('Incompatible Parameters: @Mode set to %d and @OutputType set to NONE',12,1,@Mode); + RETURN; + END; + */ +END; + +IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL + DROP TABLE #IndexSanity; + +IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL + DROP TABLE #IndexPartitionSanity; + +IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL + DROP TABLE #IndexSanitySize; + +IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL + DROP TABLE #IndexColumns; + +IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL + DROP TABLE #MissingIndexes; + +IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL + DROP TABLE #ForeignKeys; + +IF OBJECT_ID('tempdb..#UnindexedForeignKeys') IS NOT NULL + DROP TABLE #UnindexedForeignKeys; + +IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL + DROP TABLE #BlitzIndexResults; + +IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL + DROP TABLE #IndexCreateTsql; + +IF OBJECT_ID('tempdb..#DatabaseList') IS NOT NULL + DROP TABLE #DatabaseList; + +IF OBJECT_ID('tempdb..#Statistics') IS NOT NULL + DROP TABLE #Statistics; + +IF OBJECT_ID('tempdb..#PartitionCompressionInfo') IS NOT NULL + DROP TABLE #PartitionCompressionInfo; + +IF OBJECT_ID('tempdb..#ComputedColumns') IS NOT NULL + DROP TABLE #ComputedColumns; + +IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL + DROP TABLE #TraceStatus; + +IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL + DROP TABLE #TemporalTables; + +IF OBJECT_ID('tempdb..#CheckConstraints') IS NOT NULL + DROP TABLE #CheckConstraints; + +IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL + DROP TABLE #FilteredIndexes; + +IF OBJECT_ID('tempdb..#Ignore_Databases') IS NOT NULL + 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 + DROP TABLE #dm_db_index_operational_stats + + RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; + CREATE TABLE #BlitzIndexResults + ( + blitz_result_id INT IDENTITY PRIMARY KEY, + check_id INT NOT NULL, + index_sanity_id INT NULL, + Priority INT NULL, + findings_group NVARCHAR(4000) NOT NULL, + finding NVARCHAR(200) NOT NULL, + [database_name] NVARCHAR(128) NULL, + URL NVARCHAR(200) NOT NULL, + details NVARCHAR(MAX) NOT NULL, + index_definition NVARCHAR(MAX) NOT NULL, + secret_columns NVARCHAR(MAX) NULL, + index_usage_summary NVARCHAR(MAX) NULL, + index_size_summary NVARCHAR(MAX) NULL, + create_tsql NVARCHAR(MAX) NULL, + more_info NVARCHAR(MAX) NULL, + sample_query_plan XML NULL + ); + + CREATE TABLE #IndexSanity + ( + [index_sanity_id] INT IDENTITY PRIMARY KEY CLUSTERED, + [database_id] SMALLINT NOT NULL , + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [index_type] TINYINT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [object_name] NVARCHAR(128) NOT NULL , + index_name NVARCHAR(128) NULL , + key_column_names NVARCHAR(MAX) NULL , + key_column_names_with_sort_order NVARCHAR(MAX) NULL , + key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , + count_key_columns INT NULL , + include_column_names NVARCHAR(MAX) NULL , + include_column_names_no_types NVARCHAR(MAX) NULL , + count_included_columns INT NULL , + partition_key_column_name NVARCHAR(MAX) NULL, + filter_definition NVARCHAR(MAX) NOT NULL , + optimize_for_sequential_key BIT NULL, + is_indexed_view BIT NOT NULL , + is_unique BIT NOT NULL , + is_primary_key BIT NOT NULL , + is_unique_constraint BIT NOT NULL , + is_XML bit NOT NULL, + is_spatial BIT NOT NULL, + is_NC_columnstore BIT NOT NULL, + is_CX_columnstore BIT NOT NULL, + is_in_memory_oltp BIT NOT NULL , + is_disabled BIT NOT NULL , + is_hypothetical BIT NOT NULL , + is_padded BIT NOT NULL , + fill_factor SMALLINT NOT NULL , + user_seeks BIGINT NOT NULL , + user_scans BIGINT NOT NULL , + user_lookups BIGINT NOT NULL , + user_updates BIGINT NULL , + last_user_seek DATETIME NULL , + last_user_scan DATETIME NULL , + last_user_lookup DATETIME NULL , + last_user_update DATETIME NULL , + is_referenced_by_foreign_key BIT DEFAULT(0), + secret_columns NVARCHAR(MAX) NULL, + count_secret_columns INT NULL, + create_date DATETIME NOT NULL, + modify_date DATETIME NOT NULL, + filter_columns_not_in_index NVARCHAR(MAX), + [db_schema_object_name] AS [schema_name] + N'.' + [object_name] , + [db_schema_object_indexid] AS [schema_name] + N'.' + [object_name] + + CASE WHEN [index_name] IS NOT NULL THEN N'.' + index_name + ELSE N'' + END + N' (' + CAST(index_id AS NVARCHAR(20)) + N')' , + first_key_column_name AS CASE WHEN count_key_columns > 1 + THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) + ELSE key_column_names + END , + index_definition AS + CASE WHEN partition_key_column_name IS NOT NULL + THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' + ELSE '' + END + + CASE index_id + WHEN 0 THEN N'[HEAP] ' + WHEN 1 THEN N'[CX] ' + ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN N'[VIEW] ' + ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' + 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_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] ' + ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 AND is_unique_constraint = 0 THEN N'[UNIQUE] ' + ELSE N'' END + CASE WHEN is_unique_constraint = 1 AND is_primary_key = 0 THEN N'[UNIQUE CONSTRAINT] ' + ELSE N'' END + CASE WHEN count_key_columns > 0 THEN + N'[' + CAST(count_key_columns AS NVARCHAR(10)) + N' KEY' + + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END + + N'] ' + LTRIM(key_column_names_with_sort_order) + ELSE N'' END + CASE WHEN count_included_columns > 0 THEN + N' [' + CAST(count_included_columns AS NVARCHAR(10)) + N' INCLUDE' + + + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END + + N'] ' + include_column_names + ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition + ELSE N'' END , + [total_reads] AS user_seeks + user_scans + user_lookups, + [reads_per_write] AS CAST(CASE WHEN user_updates > 0 + THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) + ELSE 0 END AS MONEY) , + [index_usage_summary] AS + CASE WHEN is_spatial = 1 THEN N'Not Tracked' + WHEN is_disabled = 1 THEN N'Disabled' + ELSE N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), N'.00', N'') + + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN + N' (' + + RTRIM( + CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), N'.00', N'') + N' seek ' ELSE N'' END + + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), N'.00', N'') + N' scan ' ELSE N'' END + + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), N'.00', N'') + N' lookup' ELSE N'' END + ) + + N') ' + ELSE N' ' + END + + N'Writes: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), N'.00', N'') + END /* First "end" is about is_spatial */, + [more_info] AS + CASE WHEN is_in_memory_oltp = 1 + THEN N'EXEC dbo.sp_BlitzInMemoryOLTP @dbName=' + QUOTENAME([database_name],N'''') + + N', @tableName=' + QUOTENAME([object_name],N'''') + N';' + ELSE N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') + + N', @SchemaName=' + QUOTENAME([schema_name],N'''') + N', @TableName=' + QUOTENAME([object_name],N'''') + N';' + END + ); + RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; + IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') + CREATE UNIQUE INDEX uq_database_id_object_id_index_id ON #IndexSanity (database_id, object_id, index_id); + + + CREATE TABLE #IndexPartitionSanity + ( + [index_partition_sanity_id] INT IDENTITY, + [index_sanity_id] INT NULL , + [database_id] INT NOT NULL , + [object_id] INT NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL, + [index_id] INT NOT NULL , + [partition_number] INT NOT NULL , + row_count BIGINT NOT NULL , + reserved_MB NUMERIC(29,2) NOT NULL , + reserved_LOB_MB NUMERIC(29,2) NOT NULL , + reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + reserved_dictionary_MB NUMERIC(29,2) NOT NULL , + leaf_insert_count BIGINT NULL , + leaf_delete_count BIGINT NULL , + leaf_update_count BIGINT NULL , + range_scan_count BIGINT NULL , + singleton_lookup_count BIGINT NULL , + forwarded_fetch_count BIGINT NULL , + lob_fetch_in_pages BIGINT NULL , + lob_fetch_in_bytes BIGINT NULL , + row_overflow_fetch_in_pages BIGINT NULL , + row_overflow_fetch_in_bytes BIGINT NULL , + row_lock_count BIGINT NULL , + row_lock_wait_count BIGINT NULL , + row_lock_wait_in_ms BIGINT NULL , + page_lock_count BIGINT NULL , + page_lock_wait_count BIGINT NULL , + page_lock_wait_in_ms BIGINT NULL , + index_lock_promotion_attempt_count BIGINT NULL , + index_lock_promotion_count BIGINT NULL, + data_compression_desc NVARCHAR(60) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL + ); + + CREATE TABLE #IndexSanitySize + ( + [index_sanity_size_id] INT IDENTITY NOT NULL , + [index_sanity_id] INT NULL , + [database_id] INT NOT NULL, + [schema_name] NVARCHAR(128) NOT NULL, + partition_count INT NOT NULL , + total_rows BIGINT NOT NULL , + total_reserved_MB NUMERIC(29,2) NOT NULL , + total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , + total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + total_reserved_dictionary_MB NUMERIC(29,2) NOT NULL , + total_leaf_delete_count BIGINT NULL, + total_leaf_update_count BIGINT NULL, + total_range_scan_count BIGINT NULL, + total_singleton_lookup_count BIGINT NULL, + total_forwarded_fetch_count BIGINT NULL, + total_row_lock_count BIGINT NULL , + total_row_lock_wait_count BIGINT NULL , + total_row_lock_wait_in_ms BIGINT NULL , + avg_row_lock_wait_in_ms BIGINT NULL , + total_page_lock_count BIGINT NULL , + total_page_lock_wait_count BIGINT NULL , + total_page_lock_wait_in_ms BIGINT NULL , + avg_page_lock_wait_in_ms BIGINT NULL , + total_index_lock_promotion_attempt_count BIGINT NULL , + total_index_lock_promotion_count BIGINT NULL , + data_compression_desc NVARCHAR(4000) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL, + index_size_summary AS ISNULL( + CASE WHEN partition_count > 1 + THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' + ELSE N'' + END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS MONEY), 1), N'.00', N'') + N' rows; ' + + CASE WHEN total_reserved_MB > 1024 THEN + CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' + ELSE + CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' + END + + CASE WHEN total_reserved_LOB_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END + WHEN total_reserved_LOB_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END + ELSE '' + END + + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' + WHEN total_reserved_row_overflow_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' + ELSE '' + END + + CASE WHEN total_reserved_dictionary_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Dictionaries' + WHEN total_reserved_dictionary_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Dictionaries' + ELSE '' + END , + N'Error- NULL in computed column'), + index_op_stats AS ISNULL( + ( + REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' + + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN + REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' + ELSE N'' END + + /* rows will only be in this dmv when data is in memory for the table */ + ), N'Table metadata not in memory'), + index_lock_wait_summary AS ISNULL( + CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND + total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits; ' + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN N'Lock escalation DISABLE.' + ELSE N'' + END + ELSE + CASE WHEN total_row_lock_wait_count > 0 THEN + N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_page_lock_wait_count > 0 THEN + N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS MONEY), 1), N'.00', N'') + + N'; total duration: ' + + CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + + N'avg duration: ' + + CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ + REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' + ELSE + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' + END + ELSE N'' + END + + CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN + N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') + + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') +N'; ' + ELSE N'' + END + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN + N'Lock escalation is disabled.' + ELSE N'' + END + END + ,'Error- NULL in computed column') + ); + + CREATE TABLE #IndexColumns + ( + [database_id] INT NOT NULL, + [schema_name] NVARCHAR(128), + [object_id] INT NOT NULL , + [index_id] INT NOT NULL , + [key_ordinal] INT NULL , + is_included_column BIT NULL , + is_descending_key BIT NULL , + [partition_ordinal] INT NULL , + column_name NVARCHAR(256) NOT NULL , + system_type_name NVARCHAR(256) NOT NULL, + max_length SMALLINT NOT NULL, + [precision] TINYINT NOT NULL, + [scale] TINYINT NOT NULL, + collation_name NVARCHAR(256) NULL, + is_nullable BIT NULL, + is_identity BIT NULL, + is_computed BIT NULL, + is_replicated BIT NULL, + is_sparse BIT NULL, + is_filestream BIT NULL, + seed_value DECIMAL(38,0) NULL, + increment_value DECIMAL(38,0) NULL , + last_value DECIMAL(38,0) NULL, + is_not_for_replication BIT NULL + ); + CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns + (database_id, object_id, index_id); + + CREATE TABLE #MissingIndexes + ([database_id] INT NOT NULL, + [object_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + [table_name] NVARCHAR(128), + [statement] NVARCHAR(512) NOT NULL, + magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), + avg_total_user_cost NUMERIC(29,4) NOT NULL, + avg_user_impact NUMERIC(29,1) NOT NULL, + user_seeks BIGINT NOT NULL, + user_scans BIGINT NOT NULL, + unique_compiles BIGINT NULL, + equality_columns NVARCHAR(MAX), + equality_columns_with_data_type NVARCHAR(MAX), + inequality_columns NVARCHAR(MAX), + inequality_columns_with_data_type NVARCHAR(MAX), + included_columns NVARCHAR(MAX), + included_columns_with_data_type NVARCHAR(MAX), + is_low BIT, + [index_estimated_impact] AS + REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (user_seeks + user_scans) + AS BIGINT) AS MONEY), 1), '.00', '') + N' use' + + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END + +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) + + N'%; Avg query cost: ' + + CAST(avg_total_user_cost AS NVARCHAR(30)), + [missing_index_details] AS + CASE WHEN COALESCE(equality_columns_with_data_type,equality_columns) IS NOT NULL + THEN N'EQUALITY: ' + COALESCE(CAST(equality_columns_with_data_type AS NVARCHAR(MAX)), CAST(equality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(inequality_columns_with_data_type,inequality_columns) IS NOT NULL + THEN N'INEQUALITY: ' + COALESCE(CAST(inequality_columns_with_data_type AS NVARCHAR(MAX)), CAST(inequality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(included_columns_with_data_type,included_columns) IS NOT NULL + THEN N'INCLUDE: ' + COALESCE(CAST(included_columns_with_data_type AS NVARCHAR(MAX)), CAST(included_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END, + [create_tsql] AS N'CREATE INDEX [' + + LEFT(REPLACE(REPLACE(REPLACE(REPLACE( + ISNULL(equality_columns,N'')+ + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END + + ISNULL(inequality_columns,''),',','') + ,'[',''),']',''),' ','_') + + CASE WHEN included_columns IS NOT NULL THEN N'_Includes' ELSE N'' END, 128) + N'] ON ' + + [statement] + N' (' + ISNULL(equality_columns,N'') + + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END + + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + + ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END + + N' WITH (' + + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' + + N')' + + N';' + , + [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + + N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';', + [sample_query_plan] XML NULL + ); + + CREATE TABLE #ForeignKeys ( + [database_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + foreign_key_name NVARCHAR(256), + parent_object_id INT, + parent_object_name NVARCHAR(256), + referenced_object_id INT, + referenced_object_name NVARCHAR(256), + is_disabled BIT, + is_not_trusted BIT, + is_not_for_replication BIT, + parent_fk_columns NVARCHAR(MAX), + referenced_fk_columns NVARCHAR(MAX), + update_referential_action_desc NVARCHAR(16), + delete_referential_action_desc NVARCHAR(60) + ); + + CREATE TABLE #UnindexedForeignKeys + ( + [database_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + foreign_key_name NVARCHAR(256), + parent_object_name NVARCHAR(256), + parent_object_id INT, + referenced_object_name NVARCHAR(256), + referenced_object_id INT + ); + + CREATE TABLE #IndexCreateTsql ( + index_sanity_id INT NOT NULL, + create_tsql NVARCHAR(MAX) NOT NULL + ); + + CREATE TABLE #DatabaseList ( + DatabaseName NVARCHAR(256), + secondary_role_allow_connections_desc NVARCHAR(50) + + ); + + CREATE TABLE #PartitionCompressionInfo ( + [index_sanity_id] INT NULL, + [partition_compression_detail] NVARCHAR(4000) 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, + column_names NVARCHAR(MAX) NULL, + statistics_name NVARCHAR(128) NULL, + last_statistics_update DATETIME NULL, + days_since_last_stats_update INT NULL, + rows BIGINT NULL, + rows_sampled BIGINT NULL, + percent_sampled DECIMAL(18, 1) NULL, + histogram_steps INT NULL, + modification_counter BIGINT NULL, + percent_modifications DECIMAL(18, 1) 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, + persisted_sample_percent FLOAT NULL, + is_incremental BIT NULL + ); + + CREATE TABLE #ComputedColumns + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + table_name NVARCHAR(128) NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + column_name NVARCHAR(128) NULL, + is_nullable BIT NULL, + definition NVARCHAR(MAX) NULL, + uses_database_collation BIT NOT NULL, + is_persisted BIT NOT NULL, + is_computed BIT NOT NULL, + is_function INT NOT NULL, + column_definition NVARCHAR(MAX) NULL + ); + + CREATE TABLE #TraceStatus + ( + TraceFlag NVARCHAR(10) , + status BIT , + Global BIT , + Session BIT + ); + + CREATE TABLE #TemporalTables + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NOT NULL, + database_id INT NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + table_name NVARCHAR(128) NOT NULL, + history_table_name NVARCHAR(128) 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, + history_table_object_id INT NULL + ); + + CREATE TABLE #CheckConstraints + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + table_name NVARCHAR(128) NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + constraint_name NVARCHAR(128) NULL, + is_disabled BIT NULL, + definition NVARCHAR(MAX) NULL, + uses_database_collation BIT NOT NULL, + is_not_trusted BIT NOT NULL, + is_function INT NOT NULL, + column_definition NVARCHAR(MAX) NULL + ); + + CREATE TABLE #FilteredIndexes + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + table_name NVARCHAR(128) NOT NULL, + index_name NVARCHAR(128) 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), + Reason NVARCHAR(100) + ); + +/* Sanitize our inputs */ +SELECT + @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); + + +IF @GetAllDatabases = 1 + BEGIN + INSERT INTO #DatabaseList (DatabaseName) + SELECT DB_NAME(database_id) + FROM sys.databases + WHERE user_access_desc = 'MULTI_USER' + AND state_desc = 'ONLINE' + AND database_id > 4 + AND DB_NAME(database_id) NOT LIKE 'ReportServer%' + AND DB_NAME(database_id) NOT LIKE 'rdsadmin%' + AND LOWER(name) NOT IN('dbatools', 'dbadmin', 'dbmaintenance') + AND is_distributor = 0 + OPTION ( RECOMPILE ); + + /* Skip non-readable databases in an AG - see Github issue #1160 */ + IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') + BEGIN + SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( + SELECT DB_NAME(d.database_id) + FROM sys.dm_hadr_availability_replica_states rs + INNER JOIN sys.databases d ON rs.replica_id = d.replica_id + INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id + WHERE rs.role_desc = ''SECONDARY'' + AND r.secondary_role_allow_connections_desc = ''NO'') + OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql; + + IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, + 0, + N'Skipped non-readable AG secondary databases.', + N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', + N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', + 'http://FirstResponderKit.org', '', '', '', '' + ); + END; + END; + + IF @IgnoreDatabases IS NOT NULL + AND LEN(@IgnoreDatabases) > 0 + BEGIN + RAISERROR(N'Setting up filter to ignore databases', 0, 1) WITH NOWAIT; + SET @DatabaseToIgnore = ''; + + WHILE LEN(@IgnoreDatabases) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreDatabases) > 0 + BEGIN + SET @DatabaseToIgnore = SUBSTRING(@IgnoreDatabases, 0, PATINDEX('%,%',@IgnoreDatabases)) ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + + SET @IgnoreDatabases = SUBSTRING(@IgnoreDatabases, LEN(@DatabaseToIgnore + ',') + 1, LEN(@IgnoreDatabases)) ; + END; + ELSE + BEGIN + SET @DatabaseToIgnore = @IgnoreDatabases ; + SET @IgnoreDatabases = NULL ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + END; + END; + + END + + END; +ELSE + BEGIN + INSERT INTO #DatabaseList + ( DatabaseName ) + SELECT CASE + WHEN @DatabaseName IS NULL OR @DatabaseName = N'' + THEN DB_NAME() + ELSE @DatabaseName END; + END; + +SET @NumDatabases = (SELECT COUNT(*) FROM #DatabaseList AS D LEFT OUTER JOIN #Ignore_Databases AS I ON D.DatabaseName = I.DatabaseName WHERE I.DatabaseName IS NULL AND ISNULL(D.secondary_role_allow_connections_desc, 'YES') != 'NO'); +SET @msg = N'Number of databases to examine: ' + CAST(@NumDatabases AS NVARCHAR(50)); +RAISERROR (@msg,0,1) WITH NOWAIT; + + + +/* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ + + +BEGIN TRY + IF @NumDatabases >= 50 AND @BringThePain != 1 AND @TableName IS NULL + BEGIN + + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, + 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16), GETDATE(), 121) END, + N'http://FirstResponderKit.org', + N'From Your Community Volunteers', + N'', + N'', + N'' + ); + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, + 0, + N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', + N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server and/or user.', + '', + 'http://FirstResponderKit.org', + N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', + '', + '', + '' + ); + + if(@OutputType <> 'NONE') + BEGIN + SELECT bir.blitz_result_id, + bir.check_id, + bir.index_sanity_id, + bir.Priority, + bir.findings_group, + bir.finding, + bir.database_name, + bir.URL, + bir.details, + bir.index_definition, + bir.secret_columns, + bir.index_usage_summary, + bir.index_size_summary, + bir.create_tsql, + bir.more_info + FROM #BlitzIndexResults AS bir; + RAISERROR('Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server', 12, 1); + END; + + RETURN; + + END; +END TRY +BEGIN CATCH + RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; + + SELECT @msg = ERROR_MESSAGE(), + @ErrorSeverity = ERROR_SEVERITY(), + @ErrorState = ERROR_STATE(); + + RAISERROR (@msg, @ErrorSeverity, @ErrorState); + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; + END CATCH; + + +RAISERROR (N'Checking partition counts to exclude databases with over 100 partitions',0,1) WITH NOWAIT; +IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL + BEGIN + DECLARE partition_cursor CURSOR FOR + SELECT dl.DatabaseName + FROM #DatabaseList dl + LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName + WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL + + OPEN partition_cursor + FETCH NEXT FROM partition_cursor INTO @DatabaseName + + WHILE @@FETCH_STATUS = 0 + BEGIN + /* Count the total number of partitions */ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; + IF @Rowcount > 100 + BEGIN + RAISERROR (N'Skipping database %s because > 100 partitions were found. To check this database, you must set @BringThePain = 1.',0,1,@DatabaseName) WITH NOWAIT; + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT @DatabaseName, 'Over 100 partitions found - use @BringThePain = 1 to analyze' + END; + FETCH NEXT FROM partition_cursor INTO @DatabaseName + END; + CLOSE partition_cursor + DEALLOCATE partition_cursor + + END; + +INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) +SELECT 1, 0 , + 'Database Skipped', + i.DatabaseName, + 'http://FirstResponderKit.org', + i.Reason, '', '', '' +FROM #Ignore_Databases i; + + +/* Last startup */ +IF COLUMNPROPERTY(OBJECT_ID('sys.dm_os_sys_info'),'sqlserver_start_time','ColumnID') IS NOT NULL +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, sqlserver_start_time, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.dm_os_sys_info; +END +ELSE +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.databases + WHERE database_id = 2; +END + +IF @DaysUptime = 0 OR @DaysUptime IS NULL + SET @DaysUptime = .01; + +SELECT @DaysUptimeInsertValue = 'Server: ' + (CONVERT(VARCHAR(256), (SERVERPROPERTY('ServerName')))) + ' Days Uptime: ' + RTRIM(@DaysUptime); + + +/* Permission granted or unnecessary? Ok, let's go! */ + +RAISERROR (N'Starting loop through databases',0,1) WITH NOWAIT; +DECLARE c1 CURSOR +LOCAL FAST_FORWARD +FOR +SELECT dl.DatabaseName +FROM #DatabaseList dl +LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName +WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL +ORDER BY dl.DatabaseName; + +OPEN c1; +FETCH NEXT FROM c1 INTO @DatabaseName; + WHILE @@FETCH_STATUS = 0 + +BEGIN + + RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; + RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; + RAISERROR (@DatabaseName, 0, 1) WITH NOWAIT; + +SELECT @DatabaseID = [database_id] +FROM sys.databases + WHERE [name] = @DatabaseName + AND user_access_desc='MULTI_USER' + AND state_desc = 'ONLINE'; + +---------------------------------------- +--STEP 1: OBSERVE THE PATIENT +--This step puts index information into temp tables. +---------------------------------------- +BEGIN TRY + BEGIN + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'starting at %s',0,1, @d) WITH NOWAIT; + + --Validate SQL Server Version + + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 + )) <= 9 + BEGIN + SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2008 and higher. The version of this instance is: ' + @SQLServerProductVersion; + RAISERROR(@msg,16,1); + END; + + --Short circuit here if database name does not exist. + IF @DatabaseName IS NULL OR @DatabaseID IS NULL + BEGIN + SET @msg='Database does not exist or is not online/multi-user: cannot proceed.'; + RAISERROR(@msg,16,1); + END; + + --Validate parameters. + IF (@Mode NOT IN (0,1,2,3,4)) + BEGIN + SET @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail, 4=diagnose detail'; + RAISERROR(@msg,16,1); + END; + + IF (@Mode <> 0 AND @TableName IS NOT NULL) + BEGIN + SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; + RAISERROR(@msg,16,1); + END; + + IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) + BEGIN + SET @msg=N'@Filter only applies when @Mode=0 and @TableName is not specified. Please try again.'; + RAISERROR(@msg,16,1); + END; + + IF (@SchemaName IS NOT NULL AND @TableName IS NULL) + BEGIN + SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.'; + RAISERROR(@msg,16,1); + END; + + + IF (@TableName IS NOT NULL AND @SchemaName IS NULL) + BEGIN + SET @SchemaName=N'dbo'; + SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.'; + RAISERROR(@msg,1,1) WITH NOWAIT; + END; + + --If a table is specified, grab the object id. + --Short circuit if it doesn't exist. + IF @TableName IS NOT NULL + BEGIN + SET @dsql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @ObjectID= OBJECT_ID + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on + so.schema_id=sc.schema_id + where so.type in (''U'', ''V'') + and so.name=' + QUOTENAME(@TableName,'''')+ N' + and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' + /*Has a row in sys.indexes. This lets us get indexed views.*/ + and exists ( + SELECT si.name + FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si + WHERE so.object_id=si.object_id) + OPTION (RECOMPILE);'; + + SET @params='@ObjectID INT OUTPUT'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; + + IF @ObjectID IS NULL + BEGIN + SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + + N'Please check your parameters.'; + RAISERROR(@msg,1,1); + RETURN; + END; + END; + + --set @collation + SELECT @collation=collation_name + FROM sys.databases + WHERE database_id=@DatabaseID; + + --insert columns for clustered indexes and heaps + --collect info on identity columns for this one + SET @dsql = N'/* sp_BlitzIndex */ + SET LOCK_TIMEOUT 1000; /* To fix locking bug in sys.identity_columns. See Github issue #2176. */ + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', + s.name, + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', + CAST(ic.seed_value AS DECIMAL(38,0)), + CAST(ic.increment_value AS DECIMAL(38,0)), + CAST(ic.last_value AS DECIMAL(38,0)), + ic.is_not_for_replication + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON + si.object_id=c.object_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON + c.object_id=ic.object_id and + c.column_id=ic.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + WHERE si.index_id in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N'OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 1, 4000); + PRINT SUBSTRING(@dsql, 4001, 4000); + PRINT SUBSTRING(@dsql, 8001, 4000); + PRINT SUBSTRING(@dsql, 12001, 4000); + PRINT SUBSTRING(@dsql, 16001, 4000); + PRINT SUBSTRING(@dsql, 20001, 4000); + PRINT SUBSTRING(@dsql, 24001, 4000); + PRINT SUBSTRING(@dsql, 28001, 4000); + PRINT SUBSTRING(@dsql, 32001, 4000); + PRINT SUBSTRING(@dsql, 36001, 4000); + END; + BEGIN TRY + INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) + EXEC sp_executesql @dsql; + END TRY + BEGIN CATCH + RAISERROR (N'Failure inserting data into #IndexColumns for clustered indexes and heaps.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL + BEGIN + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END; + + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), + @ErrorSeverity = 0, @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; + END CATCH; + + + --insert columns for nonclustered indexes + --this uses a full join to sys.index_columns + --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', + s.name, + si.object_id, + si.index_id, + sc.key_ordinal, + sc.is_included_column, + sc.is_descending_key, + sc.partition_ordinal, + c.name as column_name, + st.name as system_type_name, + c.max_length, + c.[precision], + c.[scale], + c.collation_name, + c.is_nullable, + c.is_identity, + c.is_computed, + c.is_replicated, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL AS is_sparse' END + N', + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL AS is_filestream' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON + si.object_id=c.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON + sc.object_id = si.object_id + and sc.index_id=si.index_id + AND sc.column_id=c.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON + c.system_type_id=st.system_type_id + AND c.user_type_id=st.user_type_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + WHERE si.index_id not in (0,1) ' + + CASE WHEN @ObjectID IS NOT NULL + THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + ELSE N'' END + + N'OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream ) + EXEC sp_executesql @dsql; + + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + so.object_id, + si.index_id, + si.type, + @i_DatabaseName AS database_name, + COALESCE(sc.name, ''Unknown'') AS [schema_name], + COALESCE(so.name, ''Unknown'') AS [object_name], + COALESCE(si.name, ''Unknown'') AS [index_name], + CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, + si.is_unique, + si.is_primary_key, + si.is_unique_constraint, + CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, + 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.data_space_id = 0 then 1 else 0 end as is_in_memory_oltp, + si.is_disabled, + si.is_hypothetical, + si.is_padded, + si.fill_factor,' + + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N' + CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition + ELSE N'''' + END AS filter_definition' ELSE N''''' AS filter_definition' END + + CASE + WHEN @OptimizeForSequentialKey = 1 + THEN N', si.optimize_for_sequential_key' + ELSE N', CONVERT(BIT, NULL) AS optimize_for_sequential_key' + END + + N', + ISNULL(us.user_seeks, 0), + ISNULL(us.user_scans, 0), + ISNULL(us.user_lookups, 0), + ISNULL(us.user_updates, 0), + us.last_user_seek, + us.last_user_scan, + us.last_user_lookup, + us.last_user_update, + so.create_date, + so.modify_date + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si WITH (NOLOCK) + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc ON so.schema_id = sc.schema_id + 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 */ ' + + 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) + AND @TableName IS NULL ) + THEN N'AND ( us.user_seeks + us.user_scans + us.user_lookups + us.user_updates ) > 0' + ELSE N'' + END + + N'OPTION ( RECOMPILE ); + '; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + 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, + 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 ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + + RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; + IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL + BEGIN + /* Count the total number of partitions */ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; + IF @Rowcount > 100 + BEGIN + RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',0,1) WITH NOWAIT; + SET @SkipPartitions = 1; + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, 0 , + 'Some Checks Were Skipped', + '@SkipPartitions Forced to 1', + 'http://FirstResponderKit.org', CAST(@Rowcount AS NVARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' + ); + END; + END; + + + + IF (@SkipPartitions = 0) + BEGIN + IF (SELECT LEFT(@SQLServerProductVersion, + CHARINDEX('.',@SQLServerProductVersion,0)-1 )) <= 2147483647 --Make change here + BEGIN + + RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; + + --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 + --This change was made because on a table with lots of partitions, the OUTER APPLY was crazy slow. + + -- get relevant columns from sys.dm_db_partition_stats, sys.partitions and sys.objects + IF OBJECT_ID('tempdb..#dm_db_partition_stats_etc') IS NOT NULL + DROP TABLE #dm_db_partition_stats_etc; + + create table #dm_db_partition_stats_etc + ( + database_id smallint not null + , object_id int not null + , sname sysname NULL + , index_id int + , partition_number int + , partition_id bigint + , row_count bigint + , reserved_MB NUMERIC(29,2) + , reserved_LOB_MB NUMERIC(29,2) + , reserved_row_overflow_MB NUMERIC(29,2) + , lock_escalation_desc nvarchar(60) + , data_compression_desc nvarchar(60) + ) + + -- get relevant info from sys.dm_db_index_operational_stats + IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL + DROP TABLE #dm_db_index_operational_stats; + create table #dm_db_index_operational_stats + ( + database_id smallint not null + , object_id int not null + , index_id int + , partition_number int + , hobt_id bigint + , leaf_insert_count bigint + , leaf_delete_count bigint + , leaf_update_count bigint + , range_scan_count bigint + , singleton_lookup_count bigint + , forwarded_fetch_count bigint + , lob_fetch_in_pages bigint + , lob_fetch_in_bytes bigint + , row_overflow_fetch_in_pages bigint + , row_overflow_fetch_in_bytes bigint + , row_lock_count bigint + , row_lock_wait_count bigint + , row_lock_wait_in_ms bigint + , page_lock_count bigint + , page_lock_wait_count bigint + , page_lock_wait_in_ms bigint + , index_lock_promotion_attempt_count bigint + , index_lock_promotion_count bigint + , page_latch_wait_count bigint + , page_latch_wait_in_ms bigint + , page_io_latch_wait_count bigint + , page_io_latch_wait_in_ms bigint + ) + + SET @dsql = N' + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_partition_stats_etc at %s'',0,1, @d) WITH NOWAIT; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #dm_db_partition_stats_etc + ( + database_id, object_id, sname, index_id, partition_number, partition_id, row_count, reserved_MB, reserved_LOB_MB, reserved_row_overflow_MB, lock_escalation_desc, data_compression_desc + ) + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + ps.object_id, + s.name as sname, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' +'; + + SET @dsql = @dsql + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + N' + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; + + insert into #dm_db_index_operational_stats + ( + database_id + , object_id + , index_id + , partition_number + , hobt_id + , leaf_insert_count + , leaf_delete_count + , leaf_update_count + , range_scan_count + , singleton_lookup_count + , forwarded_fetch_count + , lob_fetch_in_pages + , lob_fetch_in_bytes + , row_overflow_fetch_in_pages + , row_overflow_fetch_in_bytes + , row_lock_count + , row_lock_wait_count + , row_lock_wait_in_ms + , page_lock_count + , page_lock_wait_count + , page_lock_wait_in_ms + , index_lock_promotion_attempt_count + , index_lock_promotion_count + , page_latch_wait_count + , page_latch_wait_in_ms + , page_io_latch_wait_count + , page_io_latch_wait_in_ms + ) + + select os.database_id + , os.object_id + , os.index_id + , os.partition_number ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', os.hobt_id ' + ELSE N', NULL AS hobt_id ' + END + N' + , os.leaf_insert_count + , os.leaf_delete_count + , os.leaf_update_count + , os.range_scan_count + , os.singleton_lookup_count + , os.forwarded_fetch_count + , os.lob_fetch_in_pages + , os.lob_fetch_in_bytes + , os.row_overflow_fetch_in_pages + , os.row_overflow_fetch_in_bytes + , os.row_lock_count + , os.row_lock_wait_count + , os.row_lock_wait_in_ms + , os.page_lock_count + , os.page_lock_wait_count + , os.page_lock_wait_in_ms + , os.index_lock_promotion_attempt_count + , os.index_lock_promotion_count + , os.page_latch_wait_count + , os.page_latch_wait_in_ms + , os.page_io_latch_wait_count + , os.page_io_latch_wait_in_ms + from ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats('+ CAST(@DatabaseID AS NVARCHAR(10)) +', NULL, NULL,NULL) AS os + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''finished getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; + '; + END; + ELSE + BEGIN + RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; + --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. + --If you have a lot of partitions and this suddenly starts running for a long time, change it back. + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, + ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.row_count, + ps.reserved_page_count * 8. / 1024. AS reserved_MB, + ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, + ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N', + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms)'; + + /* Get columnstore dictionary size - more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2585 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'column_store_dictionaries') + SET @dsql = @dsql + N' COALESCE((SELECT SUM (on_disk_size / 1024.0 / 1024) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries dict WHERE dict.partition_id = ps.partition_id),0) AS reserved_dictionary_MB '; + ELSE + SET @dsql = @dsql + N' 0 AS reserved_dictionary_MB '; + + + SET @dsql = @dsql + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id + AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ + AND so.type <> ''TF'' /*Exclude table valued functions*/ + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' + + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le + WHERE 1=1 + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ); + '; + END; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + EXEC sp_executesql @dsql; + INSERT #IndexPartitionSanity ( [database_id], + [object_id], + [schema_name], + index_id, + partition_number, + row_count, + reserved_MB, + reserved_LOB_MB, + reserved_row_overflow_MB, + lock_escalation_desc, + data_compression_desc, + leaf_insert_count, + leaf_delete_count, + leaf_update_count, + range_scan_count, + singleton_lookup_count, + forwarded_fetch_count, + lob_fetch_in_pages, + lob_fetch_in_bytes, + row_overflow_fetch_in_pages, + row_overflow_fetch_in_bytes, + row_lock_count, + row_lock_wait_count, + row_lock_wait_in_ms, + page_lock_count, + page_lock_wait_count, + page_lock_wait_in_ms, + index_lock_promotion_attempt_count, + index_lock_promotion_count, + page_latch_wait_count, + page_latch_wait_in_ms, + page_io_latch_wait_count, + page_io_latch_wait_in_ms, + reserved_dictionary_MB) + select h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc, + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms) + ,COALESCE((SELECT SUM (dict.on_disk_size / 1024.0 / 1024) FROM sys.column_store_dictionaries dict WHERE dict.partition_id = h.partition_id),0) AS reserved_dictionary_MB + from #dm_db_partition_stats_etc h + left JOIN #dm_db_index_operational_stats as os ON + h.object_id=os.object_id and h.index_id=os.index_id and h.partition_number=os.partition_number + group by h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.partition_id, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc + + END; --End Check For @SkipPartitions = 0 + + + IF @Mode NOT IN(1, 2) + BEGIN + RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; + SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + + + SET @dsql = @dsql + ' +WITH + ColumnNamesWithDataTypes AS +( + SELECT + id.index_handle, + id.object_id, + cn.IndexColumnType, + STUFF + ( + ( + SELECT + '', '' + + cn_inner.ColumnName + + '' '' + + N'' {'' + + CASE + WHEN ty.name IN (''varchar'', ''char'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''nvarchar'', ''nchar'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length / 2 AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''decimal'', ''numeric'') + THEN ty.name + + ''('' + + CAST(co.precision AS VARCHAR(25)) + + '', '' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + WHEN ty.name IN (''datetime2'') + THEN ty.name + + ''('' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + ELSE ty.name END + ''}'' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id_inner + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(N''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + ) AS cn_inner + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS co + ON co.object_id = id_inner.object_id + AND ''['' + co.name + '']'' = cn_inner.ColumnName + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS ty + 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 cn_inner.IndexColumnType = cn.IndexColumnType + FOR XML PATH('''') + ), + 1, + 1, + '''' + ) AS ReplaceColumnNames + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + )AS cn + WHERE id.database_id = DB_ID(''' + QUOTENAME(@DatabaseName) + N''') + GROUP BY + id.index_handle, + id.object_id, + cn.IndexColumnType +) +SELECT + * +INTO #ColumnNamesWithDataTypes +FROM ColumnNamesWithDataTypes +OPTION(RECOMPILE); + +SELECT + id.database_id, + id.object_id, + @i_DatabaseName, + sc.[name], + so.[name], + id.statement, + gs.avg_total_user_cost, + gs.avg_user_impact, + gs.user_seeks, + gs.user_scans, + gs.unique_compiles, + id.equality_columns, + id.inequality_columns, + id.included_columns, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Equality'' + ) AS equality_columns_with_data_type, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Inequality'' + ) AS inequality_columns_with_data_type, + ( + SELECT ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Included'' + ) AS included_columns_with_data_type,'; + + /* Get the sample query plan if it's available, and if there are less than 1,000 rows in the DMV: */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.all_objects AS o + WHERE o.name = 'dm_db_missing_index_group_stats_query' + ) + SELECT + @dsql += N' + NULL AS sample_query_plan' + ELSE + BEGIN + /* The DMV is only supposed to have 600 rows in it. If it's got more, + they could see performance slowdowns - see Github #3085. */ + DECLARE @MissingIndexPlans BIGINT; + SET @StringToExecute = N'SELECT @MissingIndexPlans = COUNT(*) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query;' + EXEC sp_executesql @StringToExecute, N'@MissingIndexPlans BIGINT OUT', @MissingIndexPlans OUT; + + IF @MissingIndexPlans > 1000 + BEGIN + SELECT @dsql += N' + NULL AS sample_query_plan /* Over 1000 plans found, skipping */'; + RAISERROR (N'Over 1000 plans found in sys.dm_db_missing_index_group_stats_query - your SQL Server is hitting a bug: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3085',0,1) WITH NOWAIT; + END + ELSE + SELECT + @dsql += N' + sample_query_plan = + ( + SELECT TOP (1) + p.query_plan + FROM sys.dm_db_missing_index_group_stats gs + CROSS APPLY + ( + SELECT TOP (1) + s.plan_handle + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query q + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_exec_query_stats s + ON q.query_plan_hash = s.query_plan_hash + WHERE gs.group_handle = q.group_handle + ORDER BY + (q.user_seeks + q.user_scans) DESC, + s.total_logical_reads DESC + ) q2 + CROSS APPLY sys.dm_exec_query_plan(q2.plan_handle) p + WHERE ig.index_group_handle = gs.group_handle + )' + END + + + + SET @dsql = @dsql + N' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_groups ig +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details id + ON ig.index_handle = id.index_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats gs + ON ig.index_group_handle = gs.group_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so + ON id.object_id=so.object_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc + ON so.schema_id=sc.schema_id +WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + +CASE + WHEN @ObjectID IS NULL + THEN N'' + ELSE N' +AND id.object_id = ' + CAST(@ObjectID AS NVARCHAR(30)) +END + +N' +OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, + avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, + inequality_columns, included_columns, equality_columns_with_data_type, inequality_columns_with_data_type, + included_columns_with_data_type, sample_query_plan) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + + SET @dsql = N' + SELECT DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') 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 + 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 + OPTION (RECOMPILE);'; + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, + is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, + [update_referential_action_desc], [delete_referential_action_desc] ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql = N' + SELECT + DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N') 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')), + fk.parent_object_id, + foreign_key_referenced_table = + OBJECT_NAME(fk.referenced_object_id, DB_ID(N' + QUOTENAME(@DatabaseName,'''') + N')), + fk.referenced_object_id + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_keys fk + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = fk.schema_id + WHERE fk.is_disabled = 0 + AND EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + WHERE fkc.constraint_object_id = fk.object_id + AND NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic + WHERE ic.object_id = fkc.parent_object_id + AND ic.column_id = fkc.parent_column_id + AND ic.index_column_id = fkc.constraint_column_id + ) + ) + OPTION (RECOMPILE);' + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #UnindexedForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + INSERT + #UnindexedForeignKeys + ( + database_id, + database_name, + schema_name, + foreign_key_name, + parent_object_name, + parent_object_id, + referenced_object_name, + referenced_object_id + ) + EXEC sys.sp_executesql + @dsql, + N'@i_DatabaseName sysname', + @DatabaseName; + END; + + + IF @Mode NOT IN(1, 2) + BEGIN + IF @SkipStatistics = 0 /* AND DB_NAME() = @DatabaseName /* Can only get stats in the current database - see https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1947 */ */ + BEGIN + IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) + OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) + OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) + 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, 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, 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, + ca.column_names AS column_names, + s.name AS statistics_name, + CONVERT(DATETIME, ddsp.last_updated) AS last_statistics_update, + DATEDIFF(DAY, ddsp.last_updated, GETDATE()) AS days_since_last_stats_update, + ddsp.rows, + ddsp.rows_sampled, + CAST(ddsp.rows_sampled / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) AS percent_sampled, + ddsp.steps AS histogram_steps, + ddsp.modification_counter, + CASE WHEN ddsp.modification_counter > 0 + THEN CAST(ddsp.modification_counter / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) + ELSE ddsp.modification_counter + END AS percent_modifications, + CASE WHEN ddsp.rows < 500 THEN 500 + 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, + ' + + 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 + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch + ON sch.schema_id = obj.schema_id + LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = s.object_id + AND i.index_id = s.stats_id + OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON sc.column_id = c.column_id AND sc.object_id = c.object_id + WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id + ORDER BY sc.stats_column_id + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') + ) ca (column_names) + WHERE obj.is_ms_shipped = 0 + OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + ELSE + 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, 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, 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, + ca.column_names AS column_names, + s.name AS statistics_name, + CONVERT(DATETIME, STATS_DATE(s.object_id, s.stats_id)) AS last_statistics_update, + DATEDIFF(DAY, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_since_last_stats_update, + si.rowcnt, + si.rowmodctr, + CASE WHEN si.rowmodctr > 0 THEN CAST(si.rowmodctr / ( 1. * NULLIF(si.rowcnt, 0) ) * 100 AS DECIMAL(18, 1)) + ELSE si.rowmodctr + END AS percent_modifications, + CASE WHEN si.rowcnt < 500 THEN 500 + 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, + CONVERT(DATETIME, obj.modify_date) AS table_modify_date, + s.no_recompute, + ' + + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' + THEN N's.has_filter, + s.filter_definition,' + 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, + ' + + 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 + ON si.name = s.name AND s.object_id = si.id + INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj + ON s.object_id = obj.object_id + INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch + ON sch.schema_id = obj.schema_id + LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = s.object_id + AND i.index_id = s.stats_id + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON sc.column_id = c.column_id AND sc.object_id = c.object_id + WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id + ORDER BY sc.stats_column_id + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') + ) ca (column_names) + WHERE obj.is_ms_shipped = 0 + AND si.rowcnt > 0 + OPTION (RECOMPILE);'; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + END; + END; + + IF @Mode NOT IN(1, 2) + BEGIN + IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) + BEGIN + RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + t.name AS table_name, + s.name AS schema_name, + c.name AS column_name, + cc.is_nullable, + cc.definition, + cc.uses_database_collation, + cc.is_persisted, + cc.is_computed, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, + ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + + '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + + CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.computed_columns AS cc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON cc.object_id = c.object_id + AND cc.column_id = c.column_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = cc.object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = t.schema_id + OPTION (RECOMPILE);'; + + IF @dsql IS NULL RAISERROR('@dsql is null',16,1); + + INSERT #ComputedColumns + ( database_id, [database_name], table_name, schema_name, column_name, is_nullable, definition, + uses_database_collation, is_persisted, is_computed, is_function, column_definition ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + END; + + IF @Mode NOT IN(1, 2) + BEGIN + RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; + INSERT #TraceStatus + EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS'); + + IF (PARSENAME(@SQLServerProductVersion, 4) >= 13) + 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], + s.name AS schema_name, + t.name AS table_name, + oa.hsn as history_schema_name, + oa.htn AS history_table_name, + c1.name AS start_column_name, + c2.name AS end_column_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 + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c1 + ON t.object_id = c1.object_id + AND p.start_column_id = c1.column_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c2 + ON t.object_id = c2.object_id + AND p.end_column_id = c2.column_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + CROSS APPLY ( SELECT s2.name as hsn, t2.name htn + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t2 + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s2 + ON t2.schema_id = s2.schema_id + WHERE t2.object_id = t.history_table_id + AND t2.temporal_type = 1 /*History table*/ ) AS oa + WHERE t.temporal_type IN ( 2, 4 ) /*BOL currently points to these types, but has no definition for 4*/ + OPTION (RECOMPILE); + '; + + IF @dsql IS NULL + 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_object_id ) + + EXEC sp_executesql @dsql; + END; + + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + t.name AS table_name, + s.name AS schema_name, + cc.name AS constraint_name, + cc.is_disabled, + cc.definition, + cc.uses_database_collation, + cc.is_not_trusted, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, + ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + + '' ADD CONSTRAINT '' + QUOTENAME(cc.name) + '' CHECK '' + cc.definition + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.check_constraints AS cc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = cc.parent_object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = t.schema_id + OPTION (RECOMPILE);'; + + INSERT #CheckConstraints + ( database_id, [database_name], table_name, schema_name, constraint_name, is_disabled, definition, + uses_database_collation, is_not_trusted, is_function, column_definition ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + s.name AS missing_schema_name, + t.name AS missing_table_name, + i.name AS missing_index_name, + c.name AS missing_column_name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.sql_expression_dependencies AS sed + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = sed.referenced_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = sed.referenced_id + AND i.index_id = sed.referencing_minor_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON c.object_id = sed.referenced_id + AND c.column_id = sed.referenced_minor_id + WHERE sed.referencing_class = 7 + AND sed.referenced_class = 1 + AND i.has_filter = 1 + AND NOT EXISTS ( SELECT 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS ic + WHERE ic.index_id = sed.referencing_minor_id + AND ic.column_id = sed.referenced_minor_id + AND ic.object_id = sed.referenced_id ) + OPTION(RECOMPILE);' + + BEGIN TRY + INSERT #FilteredIndexes ( database_id, database_name, schema_name, table_name, index_name, column_name ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END TRY + 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 TRY +BEGIN CATCH + RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL + BEGIN + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END; + + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; + + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; +END CATCH; + FETCH NEXT FROM c1 INTO @DatabaseName; +END; +DEALLOCATE c1; + + + + + + +---------------------------------------- +--STEP 2: PREP THE TEMP TABLES +--EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. +---------------------------------------- + +RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names = D1.key_column_names +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') ,TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D1 ( key_column_names ); + +RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET partition_key_column_name = D1.partition_key_column_name +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 + ( partition_key_column_name ); + +RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key + WHEN 1 THEN N' DESC' + ELSE N'' + END + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order ); + +RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key + WHEN 1 THEN N' DESC' + ELSE N'' + END AS col_definition + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 0 /*Just Keys*/ + AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ + ORDER BY c.object_id, c.index_id, c.key_ordinal + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D2 ( key_column_names_with_sort_order_no_types ); + +RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET include_column_names = D3.include_column_names +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D3 ( include_column_names ); + +RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET include_column_names_no_types = D3.include_column_names_no_types +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + AND c.is_included_column = 1 /*Just includes*/ + ORDER BY c.column_name /*Order doesn't matter in includes, + this is here to make rows easy to compare.*/ + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + ) D3 ( include_column_names_no_types ); + +RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET count_included_columns = D4.count_included_columns, + count_key_columns = D4.count_key_columns +FROM #IndexSanity si + CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 + ELSE 0 + END) AS count_included_columns, + SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 + ELSE 0 + END) AS count_key_columns + FROM #IndexColumns c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.object_id = si.object_id + AND c.index_id = si.index_id + ) AS D4 ( count_included_columns, count_key_columns ); + +RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; +UPDATE #IndexPartitionSanity +SET index_sanity_id = i.index_sanity_id +FROM #IndexPartitionSanity ps + JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] + AND ps.index_id = i.index_id + AND i.database_id = ps.database_id + AND i.schema_name = ps.schema_name; + + +RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; +INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], [lock_escalation_desc], partition_count, total_rows, total_reserved_MB, + total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_reserved_dictionary_MB, total_range_scan_count, + total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, + total_forwarded_fetch_count,total_row_lock_count, + total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, + total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, + avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, + total_index_lock_promotion_count, data_compression_desc, + page_latch_wait_count, page_latch_wait_in_ms, page_io_latch_wait_count, page_io_latch_wait_in_ms) + SELECT index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc, + COUNT(*), SUM(row_count), SUM(reserved_MB), + SUM(reserved_LOB_MB) - SUM(reserved_dictionary_MB), /* Subtract columnstore dictionaries from LOB data */ + SUM(reserved_row_overflow_MB), + SUM(reserved_dictionary_MB), + SUM(range_scan_count), + SUM(singleton_lookup_count), + SUM(leaf_delete_count), + SUM(leaf_update_count), + SUM(forwarded_fetch_count), + SUM(row_lock_count), + SUM(row_lock_wait_count), + SUM(row_lock_wait_in_ms), + CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN + SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) + ELSE 0 END AS avg_row_lock_wait_in_ms, + SUM(page_lock_count), + SUM(page_lock_wait_count), + SUM(page_lock_wait_in_ms), + CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN + SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) + ELSE 0 END AS avg_page_lock_wait_in_ms, + SUM(index_lock_promotion_attempt_count), + SUM(index_lock_promotion_count), + LEFT(MAX(data_compression_info.data_compression_rollup),4000), + SUM(page_latch_wait_count), + SUM(page_latch_wait_in_ms), + SUM(page_io_latch_wait_count), + SUM(page_io_latch_wait_in_ms) + FROM #IndexPartitionSanity ipp + /* individual partitions can have distinct compression settings, just roll them into a list here*/ + OUTER APPLY (SELECT STUFF(( + SELECT N', ' + data_compression_desc + FROM #IndexPartitionSanity ipp2 + WHERE ipp.[object_id]=ipp2.[object_id] + AND ipp.[index_id]=ipp2.[index_id] + AND ipp.database_id = ipp2.database_id + AND ipp.schema_name = ipp2.schema_name + ORDER BY ipp2.partition_number + FOR XML PATH(''),TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) + data_compression_info(data_compression_rollup) + GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc + ORDER BY index_sanity_id +OPTION ( RECOMPILE ); + +RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; +UPDATE #MissingIndexes +SET is_low = CASE WHEN (user_seeks + user_scans) < 5000 + OR unique_compiles = 1 + THEN 1 + ELSE 0 + END; + +RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; +UPDATE #IndexSanity + SET is_referenced_by_foreign_key=1 +FROM #IndexSanity s +JOIN #ForeignKeys fk ON + s.object_id=fk.referenced_object_id + AND s.database_id=fk.database_id + AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns; + +RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; +UPDATE nc +SET secret_columns= + N'[' + + CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS NVARCHAR(10)) END + + CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + + CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + + CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + + /* Uniquifiers only needed on non-unique clustereds-- not heaps */ + CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END + END + , count_secret_columns= + CASE tb.index_id WHEN 0 THEN 1 ELSE + tb.count_key_columns + + CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END + END +FROM #IndexSanity AS nc +JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id + AND nc.database_id = tb.database_id + AND nc.schema_name = tb.schema_name + AND tb.index_id IN (0,1) +WHERE nc.index_id > 1; + +RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; +UPDATE tb +SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END + , count_secret_columns = 1 +FROM #IndexSanity AS tb +WHERE tb.index_id = 0 /*Heaps-- these have the RID */ + OR (tb.index_id=1 AND tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ + + +RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; +INSERT #IndexCreateTsql (index_sanity_id, create_tsql) +SELECT + index_sanity_id, + ISNULL ( + CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' + ELSE + CASE WHEN is_XML = 1 OR is_spatial = 1 OR is_in_memory_oltp = 1 THEN N'' /* Not even trying for these just yet...*/ + ELSE + CASE WHEN is_primary_key=1 THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] PRIMARY KEY ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_unique_constraint = 1 AND is_primary_key = 0 + THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] UNIQUE ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_CX_columnstore= 1 THEN + N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ELSE /*Else not a PK or cx columnstore */ + N'CREATE ' + + CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + + CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + + CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' + ELSE N'' END + + N'INDEX [' + + index_name + N'] ON ' + + QUOTENAME([database_name]) + N'.' + + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + + CASE WHEN is_NC_columnstore=1 THEN + N' (' + ISNULL(include_column_names_no_types,'') + N' )' + ELSE /*Else not columnstore */ + N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' + + CASE WHEN include_column_names_no_types IS NOT NULL THEN + N' INCLUDE (' + include_column_names_no_types + N')' + ELSE N'' + END + END /*End non-columnstore case */ + + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END + END /*End Non-PK index CASE */ + + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN + N' WITH (' + + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' + + N'ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' + + N')' + ELSE N'' END + + N';' + END /*End non-spatial and non-xml CASE */ + END, '[Unknown Error]') + AS create_tsql +FROM #IndexSanity; + +RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; +IF OBJECT_ID('tempdb..#maps') IS NOT NULL DROP TABLE #maps; +WITH maps + AS + ( + SELECT ips.index_sanity_id, + ips.partition_number, + ips.data_compression_desc, + ips.partition_number - ROW_NUMBER() OVER ( PARTITION BY ips.index_sanity_id, ips.data_compression_desc + ORDER BY ips.partition_number ) AS rn + FROM #IndexPartitionSanity AS ips + ) +SELECT * +INTO #maps +FROM maps; + +IF OBJECT_ID('tempdb..#grps') IS NOT NULL DROP TABLE #grps; +WITH grps + AS + ( + SELECT MIN(maps.partition_number) AS MinKey, + MAX(maps.partition_number) AS MaxKey, + maps.index_sanity_id, + maps.data_compression_desc + FROM #maps AS maps + GROUP BY maps.rn, maps.index_sanity_id, maps.data_compression_desc + ) +SELECT * +INTO #grps +FROM grps; + +INSERT #PartitionCompressionInfo ( index_sanity_id, partition_compression_detail ) +SELECT DISTINCT + grps.index_sanity_id, + SUBSTRING( + ( STUFF( + ( SELECT N', ' + N' Partition' + + CASE + WHEN grps2.MinKey < grps2.MaxKey + THEN + + N's ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' - ' + + CAST(grps2.MaxKey AS NVARCHAR(10)) + N' use ' + grps2.data_compression_desc + ELSE + N' ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' uses ' + grps2.data_compression_desc + END AS Partitions + FROM #grps AS grps2 + WHERE grps2.index_sanity_id = grps.index_sanity_id + ORDER BY grps2.MinKey, grps2.MaxKey + FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'), 1, 1, '')), 0, 8000) AS partition_compression_detail +FROM #grps AS grps; + +RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; +UPDATE sz +SET sz.data_compression_desc = pci.partition_compression_detail +FROM #IndexSanitySize sz +JOIN #PartitionCompressionInfo AS pci +ON pci.index_sanity_id = sz.index_sanity_id; + +RAISERROR (N'Update #IndexSanity for filtered indexes with columns not in the index definition.',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET filter_columns_not_in_index = D1.filter_columns_not_in_index +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #FilteredIndexes AS c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.table_name = si.object_name + AND c.index_name = si.index_name + ORDER BY c.index_sanity_id + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 + ( filter_columns_not_in_index ); + + +IF @Debug = 1 +BEGIN + SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults AS bir; + SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; + SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; + SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; + SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; + SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; + SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; + SELECT '#UnindexedForeignKeys' AS table_name, * FROM #UnindexedForeignKeys; + SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; + SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; + 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 '#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; +END + + +---------------------------------------- +--STEP 3: DIAGNOSE THE PATIENT +---------------------------------------- + + +BEGIN TRY +---------------------------------------- +--If @TableName is specified, just return information for that table. +--The @Mode parameter doesn't matter if you're looking at a specific table. +---------------------------------------- +IF @TableName IS NOT NULL +BEGIN + RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; + + --We do a left join here in case this is a disabled NC. + --In that case, it won't have any size info/pages allocated. + + IF (@ShowColumnstoreOnly = 0) + BEGIN + WITH table_mode_cte AS ( + SELECT + s.db_schema_object_indexid, + s.key_column_names, + s.index_definition, + ISNULL(s.secret_columns,N'') AS secret_columns, + s.fill_factor, + s.index_usage_summary, + sz.index_op_stats, + ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, + partition_compression_detail , + ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, + s.is_referenced_by_foreign_key, + (SELECT COUNT(*) + FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id + AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, + s.last_user_seek, + s.last_user_scan, + s.last_user_lookup, + s.last_user_update, + s.create_date, + s.modify_date, + sz.page_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_latch_wait_in_ms / 1000), 0), 108) AS page_latch_wait_time, + sz.page_io_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_io_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_io_latch_wait_in_ms / 1000), 0), 108) AS page_io_latch_wait_time, + ct.create_tsql, + CASE + WHEN s.is_primary_key = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND is_unique_constraint = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND s.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(s.index_name) + N' ON ' + QUOTENAME(s.[database_name]) + N'.' + + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + N';' + ELSE N'' + END AS drop_tsql, + 1 AS display_order + FROM #IndexSanity s + LEFT JOIN #IndexSanitySize sz ON + s.index_sanity_id=sz.index_sanity_id + LEFT JOIN #IndexCreateTsql ct ON + s.index_sanity_id=ct.index_sanity_id + LEFT JOIN #PartitionCompressionInfo pci ON + pci.index_sanity_id = s.index_sanity_id + WHERE s.[object_id]=@ObjectID + UNION ALL + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + + N' (' + @ScriptVersionName + ')' , + N'SQL Server First Responder Kit' , + N'http://FirstResponderKit.org' , + N'From Your Community Volunteers', + NULL,@DaysUptimeInsertValue,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + 0 AS display_order + ) + SELECT + db_schema_object_indexid AS [Details: db_schema.table.index(indexid)], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + secret_columns AS [Secret Columns], + fill_factor AS [Fillfactor], + index_usage_summary AS [Usage Stats], + index_op_stats AS [Op Stats], + index_size_summary AS [Size], + partition_compression_detail AS [Compression Type], + index_lock_wait_summary AS [Lock Waits], + is_referenced_by_foreign_key AS [Referenced by FK?], + FKs_covered_by_index AS [FK Covered by Index?], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Write], + create_date AS [Created], + modify_date AS [Last Modified], + page_latch_wait_count AS [Page Latch Wait Count], + page_latch_wait_time as [Page Latch Wait Time (D:H:M:S)], + page_io_latch_wait_count AS [Page IO Latch Wait Count], + page_io_latch_wait_time as [Page IO Latch Wait Time (D:H:M:S)], + create_tsql AS [Create TSQL], + drop_tsql AS [Drop TSQL] + FROM table_mode_cte + ORDER BY display_order ASC, key_column_names ASC + OPTION ( RECOMPILE ); + + IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL + BEGIN; + + WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + SELECT N'Missing index.' AS Finding , + N'https://www.brentozar.com/go/Indexaphobia' AS URL , + mi.[statement] + + ' Est. Benefit: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) + AS BIGINT) AS MONEY), 1), '.00', '') + END AS [Estimated Benefit], + missing_index_details AS [Missing Index Request] , + index_estimated_impact AS [Estimated Impact], + create_tsql AS [Create TSQL], + sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + WHERE mi.[object_id] = @ObjectID + AND (@ShowAllMissingIndexRequests=1 + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + OR (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000) + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); + END; + ELSE + SELECT 'No missing indexes.' AS finding; + + SELECT + column_name AS [Column Name], + (SELECT COUNT(*) + FROM #IndexColumns c2 + WHERE c2.column_name=c.column_name + AND c2.key_ordinal IS NOT NULL) + + CASE WHEN c.index_id = 1 AND c.key_ordinal IS NOT NULL THEN + -1+ (SELECT COUNT(DISTINCT index_id) + FROM #IndexColumns c3 + WHERE c3.index_id NOT IN (0,1)) + ELSE 0 END + AS [Found In], + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE '' + END + END + AS [Type], + CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], + max_length AS [Length (max bytes)], + [precision] AS [Prec], + [scale] AS [Scale], + CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], + CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], + CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], + CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], + CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], + collation_name AS [Collation] + FROM #IndexColumns AS c + WHERE index_id IN (0,1); + + IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL + BEGIN + SELECT [database_name] + N':' + parent_object_name + N': ' + foreign_key_name AS [Foreign Key], + parent_fk_columns AS [Foreign Key Columns], + referenced_object_name AS [Referenced Table], + referenced_fk_columns AS [Referenced Table Columns], + is_disabled AS [Is Disabled?], + is_not_trusted AS [Not Trusted?], + is_not_for_replication [Not for Replication?], + [update_referential_action_desc] AS [Cascading Updates?], + [delete_referential_action_desc] AS [Cascading Deletes?] + FROM #ForeignKeys + ORDER BY [Foreign Key] + OPTION ( RECOMPILE ); + END; + ELSE + SELECT 'No foreign keys.' AS finding; + + /* Show histograms for all stats on this table. More info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1900 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_histogram') + BEGIN + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT s.name AS [Stat Name], c.name AS [Leading Column Name], hist.step_number AS [Step Number], + hist.range_high_key AS [Range High Key], hist.range_rows AS [Range Rows], + hist.equal_rows AS [Equal Rows], hist.distinct_range_rows AS [Distinct Range Rows], hist.average_range_rows AS [Average Range Rows], + s.auto_created AS [Auto-Created], s.user_created AS [User-Created], + props.last_updated AS [Last Updated], props.modification_counter AS [Modification Counter], props.rows AS [Table Rows], + props.rows_sampled AS [Rows Sampled], s.stats_id AS [StatsID] + FROM sys.stats AS s + INNER JOIN sys.stats_columns sc ON s.object_id = sc.object_id AND s.stats_id = sc.stats_id AND sc.stats_column_id = 1 + INNER JOIN sys.columns c ON sc.object_id = c.object_id AND sc.column_id = c.column_id + CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS props + CROSS APPLY sys.dm_db_stats_histogram(s.[object_id], s.stats_id) AS hist + WHERE s.object_id = @ObjectID + ORDER BY s.auto_created, s.user_created, s.name, hist.step_number;'; + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + 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')) + BEGIN + RAISERROR(N'Visualizing columnstore index contents.', 0,1) WITH NOWAIT; + + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + IF EXISTS(SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = @ObjectID) + BEGIN + SELECT @ColumnList = N'''', @ColumnListWithApostrophes = N''''; + WITH DistinctColumns AS ( + SELECT DISTINCT QUOTENAME(c.name) AS column_name, QUOTENAME(c.name,'''''''') AS ColumnNameWithApostrophes, c.column_id + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON p.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id and ic.object_id = c.object_id AND ic.index_id = p.index_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types t ON c.system_type_id = t.system_type_id AND c.user_type_id = t.user_type_id + WHERE p.object_id = @ObjectID + AND EXISTS (SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg WHERE p.partition_id = seg.partition_id AND seg.column_id = ic.index_column_id) + AND p.data_compression IN (3,4) + ) + SELECT @ColumnList = @ColumnList + column_name + N'', '', + @ColumnListWithApostrophes = @ColumnListWithApostrophes + ColumnNameWithApostrophes + N'', '' + FROM DistinctColumns + ORDER BY column_id; + SELECT @PartitionCount = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions WHERE object_id = @ObjectID AND data_compression IN (3,4); + END'; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, N'@ObjectID INT, @ColumnList NVARCHAR(MAX) OUTPUT, @ColumnListWithApostrophes NVARCHAR(MAX) OUTPUT, @PartitionCount INT OUTPUT', @ObjectID, @ColumnList OUTPUT, @ColumnListWithApostrophes OUTPUT, @PartitionCount OUTPUT; + + IF @PartitionCount < 2 + SET @ShowPartitionRanges = 0; + + IF @Debug = 1 + SELECT @ColumnList AS ColumnstoreColumnList, @ColumnListWithApostrophes AS ColumnstoreColumnListWithApostrophes, @PartitionCount AS PartitionCount, @ShowPartitionRanges AS ShowPartitionRanges; + + IF @ColumnList <> '' + BEGIN + /* Remove the trailing comma */ + SET @ColumnList = LEFT(@ColumnList, LEN(@ColumnList) - 1); + SET @ColumnListWithApostrophes = LEFT(@ColumnListWithApostrophes, LEN(@ColumnListWithApostrophes) - 1); + + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT partition_number, ' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' COALESCE(range_start_op + '' '' + range_start + '' '', '''') + COALESCE(range_end_op + '' '' + range_end, '''') AS partition_range, ' ELSE N' ' END + + N' row_group_id, total_rows, deleted_rows, ' + + @ColumnList + + CASE WHEN @ShowPartitionRanges = 1 THEN N' , + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT column_name, partition_number, row_group_id, total_rows, deleted_rows, details, + range_start_op, + CASE + WHEN format_type IS NULL THEN CAST(range_start_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_start_value, format_type) END range_start, + range_end_op, + CASE + WHEN format_type IS NULL THEN CAST(range_end_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_end_value, format_type) END range_end' ELSE N' ' END + N', + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT c.name AS column_name, p.partition_number, rg.row_group_id, rg.total_rows, rg.deleted_rows, + phys.state_desc, phys.trim_reason_desc, phys.transition_to_compressed_state_desc, phys.has_vertipaq_optimization, + details = CAST(seg.min_data_id AS VARCHAR(20)) + '' to '' + CAST(seg.max_data_id AS VARCHAR(20)) + '', '' + CAST(CAST(((COALESCE(d.on_disk_size,0) + COALESCE(seg.on_disk_size,0)) / 1024.0 / 1024) AS DECIMAL(18,0)) AS VARCHAR(20)) + '' MB''' + + CASE WHEN @ShowPartitionRanges = 1 THEN N', + CASE + WHEN pp.system_type_id IN (40, 41, 42, 43, 58, 61) THEN 126 + WHEN pp.system_type_id IN (59, 62) THEN 3 + WHEN pp.system_type_id IN (60, 122) THEN 2 + ELSE NULL END format_type, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''>'' ELSE ''>='' END range_start_op, + prvs.value range_start_value, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''<='' ELSE ''<'' END range_end_op, + prve.value range_end_value ' ELSE N' ' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups rg + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON rg.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p ON rg.object_id = p.object_id AND rg.partition_number = p.partition_number + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id AND ic.object_id = c.object_id AND ic.index_id = p.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_column_store_row_group_physical_stats phys ON rg.row_group_id = phys.row_group_id AND rg.object_id = phys.object_id AND rg.partition_number = phys.partition_number AND rg.index_id = phys.index_id ' + CASE WHEN @ShowPartitionRanges = 1 THEN N' + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes i ON i.object_id = rg.object_id AND i.index_id = rg.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_schemes ps ON ps.data_space_id = i.data_space_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_functions pf ON pf.function_id = ps.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_parameters pp ON pp.function_id = pf.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prvs ON prvs.function_id = pf.function_id AND prvs.boundary_id = p.partition_number - 1 + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prve ON prve.function_id = pf.function_id AND prve.boundary_id = p.partition_number ' ELSE N' ' END + + N' LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg ON p.partition_id = seg.partition_id AND ic.index_column_id = seg.column_id AND rg.row_group_id = seg.segment_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries d ON p.hobt_id = d.hobt_id AND c.column_id = d.column_id AND seg.secondary_dictionary_id = d.dictionary_id + WHERE rg.object_id = @ObjectID + AND rg.state IN (1, 2, 3) + AND c.name IN ( ' + @ColumnListWithApostrophes + N')' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' + ) AS y ' ELSE N' ' END + N' + ) AS x + PIVOT (MAX(details) FOR column_name IN ( ' + @ColumnList + N')) AS pivot1 + ORDER BY partition_number, row_group_id;'; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + ELSE + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + END + ELSE /* No columns were found for this object */ + BEGIN + SELECT N'No compressed columnstore rowgroups were found for this object.' AS Columnstore_Visualization + UNION ALL + SELECT N'SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = ' + CAST(@ObjectID AS NVARCHAR(100)); + END + RAISERROR(N'Done visualizing columnstore index contents.', 0,1) WITH NOWAIT; + END + + IF @ShowColumnstoreOnly = 1 + RETURN; + +END; /* IF @TableName IS NOT NULL */ + + + + + + + + +ELSE /* @TableName IS NULL, so we operate in normal mode 0/1/2/3/4 */ +BEGIN + +/* Validate and check table output params */ + + + /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ + DECLARE @ValidOutputServer BIT; + DECLARE @ValidOutputLocation BIT; + DECLARE @LinkedServerDBCheck NVARCHAR(2000); + DECLARE @ValidLinkedServerDB INT; + DECLARE @tmpdbchk TABLE (cnt INT); + + IF @OutputServerName IS NOT NULL + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; + ELSE + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + EXEC(@StringToExecute); + + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + SET @OutputDatabaseName = '[tempdb]'; + SET @OutputSchemaName = '[dbo]'; + SET @ValidOutputLocation = 1; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + IF (@ValidOutputLocation = 0 AND @OutputType = 'NONE') + BEGIN + RAISERROR('Invalid output location and no output asked',12,1); + RETURN; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @RunID UNIQUEIDENTIFIER; + SET @RunID = NEWID(); + + DECLARE @TableExists BIT; + DECLARE @SchemaExists BIT; + + DECLARE @TableExistsSql NVARCHAR(MAX); + + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + SET @StringToExecute = + N'SET @SchemaExists = 0; + SET @TableExists = 0; + IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + SET @SchemaExists = 1 + IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + BEGIN + SET @TableExists = 1 + IF NOT EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.COLUMNS WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' + AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'' AND QUOTENAME(COLUMN_NAME) = ''[total_forwarded_fetch_count]'') + EXEC @@@OutputServerName@@@.@@@OutputDatabaseName@@@.dbo.sp_executesql N''ALTER TABLE @@@OutputSchemaName@@@.@@@OutputTableName@@@ ADD [total_forwarded_fetch_count] BIGINT'' + END'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + + EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT; + + + SET @TableExistsSql = + N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + SET @TableExists = 0 + ELSE + SET @TableExists = 1'; + + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputServerName@@@', @OutputServerName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputTableName@@@', @OutputTableName); + + END + + + + + IF @Mode IN (0, 4) /* DIAGNOSE */ + BEGIN; + IF @Mode IN (0, 4) /* DIAGNOSE priorities 1-100 */ + BEGIN; + RAISERROR(N'@Mode=0 or 4, running rules for priorities 1-100.', 0,1) WITH NOWAIT; + + ---------------------------------------- + --Multiple Index Personalities: Check_id 0-10 + ---------------------------------------- + RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; + WITH duplicate_indexes + AS ( SELECT [object_id], key_column_names, database_id, [schema_name] + FROM #IndexSanity AS ip + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical = 0 + AND is_disabled = 0 + AND is_primary_key = 0 + AND EXISTS ( + SELECT 1/0 + FROM #IndexSanitySize ips + WHERE ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name + AND ips.total_reserved_MB >= CASE + WHEN (@GetAllDatabases = 1 OR @Mode = 0) + THEN @ThresholdMB + ELSE ips.total_reserved_MB + END + ) + GROUP BY [object_id], key_column_names, database_id, [schema_name] + HAVING COUNT(*) > 1) + 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 1 AS check_id, + ip.index_sanity_id, + 20 AS Priority, + 'Redundant Indexes' AS findings_group, + 'Duplicate keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/duplicateindex' AS URL, + N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM duplicate_indexes di + JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] + AND ip.database_id = di.database_id + AND ip.[schema_name] = di.[schema_name] + AND di.key_column_names = ip.key_column_names + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name + /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ + WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END + AND ip.is_primary_key = 0 + ORDER BY ips.total_rows DESC, ip.[schema_name], ip.[object_name], ip.key_column_names_with_sort_order + OPTION ( RECOMPILE ); + + RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; + WITH borderline_duplicate_indexes + AS ( SELECT DISTINCT database_id, [object_id], first_key_column_name, key_column_names, + COUNT([object_id]) OVER ( PARTITION BY database_id, [object_id], first_key_column_name ) AS number_dupes + FROM #IndexSanity + WHERE index_type IN (1,2) /* Clustered, NC only*/ + AND is_hypothetical=0 + AND is_disabled=0 + AND is_primary_key = 0) + 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 2 AS check_id, + ip.index_sanity_id, + 30 AS Priority, + 'Redundant Indexes' AS findings_group, + 'Approximate Duplicate Keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/duplicateindex' AS URL, + ip.db_schema_object_indexid AS details, + ip.index_definition, + ip.secret_columns, + ip.index_usage_summary, + ips.index_size_summary + FROM #IndexSanity AS ip + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + WHERE EXISTS ( + SELECT di.[object_id] + FROM borderline_duplicate_indexes AS di + WHERE di.[object_id] = ip.[object_id] AND + di.database_id = ip.database_id AND + di.first_key_column_name = ip.first_key_column_name AND + di.key_column_names <> ip.key_column_names AND + di.number_dupes > 1 + ) + AND ip.is_primary_key = 0 + 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 + ---------------------------------------- + + RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page)', 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 11 AS check_id, + i.index_sanity_id, + 70 AS Priority, + N'Locking-Prone ' + + CASE COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) + WHEN 0 THEN N'Under-Indexing' + WHEN 1 THEN N'Under-Indexing' + WHEN 2 THEN N'Under-Indexing' + WHEN 3 THEN N'Under-Indexing' + WHEN 4 THEN N'Indexes' + WHEN 5 THEN N'Indexes' + WHEN 6 THEN N'Indexes' + WHEN 7 THEN N'Indexes' + WHEN 8 THEN N'Indexes' + WHEN 9 THEN N'Indexes' + ELSE N'Over-Indexing' + END AS findings_group, + N'Total lock wait time > 5 minutes (row + page)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AggressiveIndexes' AS URL, + (i.db_schema_object_indexid + N': ' + + sz.index_lock_wait_summary + N' NC indexes on table: ') COLLATE DATABASE_DEFAULT + + CAST(COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) + AS NVARCHAR(30)) AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 + GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id + ORDER BY SUM(total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) DESC, 4, [database_name], 8 + OPTION ( RECOMPILE ); + + + + ---------------------------------------- + --Index Hoarder: Check_id 20-29 + ---------------------------------------- + RAISERROR(N'check_id 20: >= 10 NC indexes on any given table. Yes, 10 is an arbitrary number.', 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 20 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + 10 AS Priority, + 'Over-Indexing' AS findings_group, + 'Many NC Indexes on a Single Table' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, + i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, + '' AS secret_columns, + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS MONEY), 1), N'.00', N'') + N' reads (ALL); ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS MONEY), 1), N'.00', N'') + N' writes (ALL); ', + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS MONEY), 1), N'.00', N'') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id NOT IN ( 0, 1 ) + GROUP BY db_schema_object_name, [i].[database_name] + HAVING COUNT(*) >= 10 + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 22: NC indexes with 0 reads and >= 10,000 writes', 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 22 AS check_id, + i.index_sanity_id, + 10 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC Index with High Writes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Reads: 0,' + + N' Writes: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + + N' on: ' + + i.db_schema_object_indexid + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.user_updates >= 10000 + 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 + AND @Filter <> 1 /* 1 = "ignore unused */ + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 34: Filtered index definition columns not in index definition', 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 34 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Filter Columns Not In Index Definition' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'The index ' + + QUOTENAME(i.index_name) + + N' on [' + + i.db_schema_object_name + + N'] has a filter on [' + + i.filter_definition + + N'] but is missing [' + + LTRIM(i.filter_columns_not_in_index) + + N'] from the index definition.' + 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 + 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 + ---------------------------------------- + + RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Nonclustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ + CASE WHEN (last_user_update IS NULL OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id > 1 + AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Clustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ + CASE WHEN (last_user_update IS NULL OR user_updates < 1) + THEN N'No writes have been made.' + ELSE + N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + + CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' + END + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE index_id = 1 + AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 43: Heaps with forwarded records', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0) + 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 43 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Forwarded Fetches' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CASE WHEN h.forwarded_fetch_count >= 922337203685477 THEN '>= 922,337,203,685,477' + WHEN @DaysUptime < 1 THEN CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + N' forwarded fetches against heap: ' + db_schema_object_indexid + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (h.forwarded_fetch_count /*/@DaysUptime */) + AS BIGINT) AS MONEY), 1), '.00', '') + END + N' forwarded fetches per day against heap: ' + + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND h.forwarded_fetch_count / @DaysUptime > 1000 + AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 44: Large Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 44 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Large Active Heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows >= 100000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 45 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Medium Active heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND + (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows >= 10000 AND sz.total_rows < 100000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(forwarded_fetch_count) AS forwarded_fetch_count, + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) > 0 + OR SUM(leaf_delete_count) > 0) + 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 46 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Small Active heap' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Should this table be a heap? ' + db_schema_object_indexid AS details, + i.index_definition, + 'N/A' AS secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND + (i.total_reads > 0 OR i.user_updates > 0) + AND sz.total_rows < 10000 + AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 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 47 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heap with a Nonclustered Primary Key' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' 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 + WHERE i.index_type = 2 AND i.is_primary_key = 1 + AND EXISTS + ( + SELECT 1/0 + FROM #IndexSanity AS isa + WHERE i.database_id = isa.database_id + AND i.object_id = isa.object_id + AND isa.index_id = 0 + ) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 48: Nonclustered indexes with a bad read to write ratio', 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 48 AS check_id, + i.index_sanity_id, + 100 AS Priority, + N'Over-Indexing' AS findings_group, + N'NC index with High Writes:Reads' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.total_reads) AS MONEY), 1), N'.00', N'') + + N' Writes: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + + N' on: ' + + i.db_schema_object_indexid 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 + WHERE i.total_reads > 0 /*Not totally unused*/ + AND i.user_updates >= 10000 /*Decent write activity*/ + AND i.total_reads < 10000 + AND ((i.total_reads * 10) < i.user_updates) /*10x more writes than reads*/ + 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 + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + + ---------------------------------------- + --Indexaphobia + --Missing indexes with value >= 5 million: : Check_id 50-59 + ---------------------------------------- + RAISERROR(N'check_id 50: High Value Missing Index.', 0,1) WITH NOWAIT; + WITH index_size_cte + AS ( SELECT i.database_id, + i.schema_name, + i.[object_id], + MAX(i.index_sanity_id) AS index_sanity_id, + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days, + ISNULL ( + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) + AS NVARCHAR(30))+ N' NC indexes exist (' + + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 + THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. + + AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' + ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + AS NVARCHAR(30)) + N'MB); ' + END + + CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS MONEY), 1), '.00', '') + END + + + N' Estimated Rows;' + ,N'') AS index_size_summary + FROM #IndexSanity AS i + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id AND i.database_id = sz.database_id + WHERE i.is_hypothetical = 0 + AND i.is_disabled = 0 + GROUP BY i.database_id, i.schema_name, i.[object_id]) + INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + index_usage_summary, index_size_summary, create_tsql, more_info, sample_query_plan ) + + SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], + index_estimated_impact, t.index_size_summary, create_tsql, more_info, sample_query_plan + FROM + ( + SELECT ROW_NUMBER() OVER (ORDER BY magic_benefit_number DESC) AS rownum, + 50 AS check_id, + sz.index_sanity_id, + 40 AS Priority, + N'Index Suggestion' AS findings_group, + N'High Value Missing Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/Indexaphobia' AS URL, + mi.[statement] + + N' Est. benefit per day: ' + + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (magic_benefit_number/@DaysUptime) + AS BIGINT) AS MONEY), 1), '.00', '') + END AS details, + missing_index_details AS [definition], + index_estimated_impact, + sz.index_size_summary, + mi.create_tsql, + mi.more_info, + magic_benefit_number, + mi.is_low, + mi.sample_query_plan + FROM #MissingIndexes mi + LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id + AND mi.database_id = sz.database_id + AND mi.schema_name = sz.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) + OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 + ) AS t + WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); + + + + RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; + -- Allowed Ranges: + --int -2,147,483,648 to 2,147,483,647 + --smallint -32,768 to 32,768 + --tinyint 0 to 255 + + 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 68 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Within ' + + CAST (calc1.percent_remaining AS NVARCHAR(256)) + + N' Percent End of Range' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + CROSS APPLY ( + SELECT CAST(CASE WHEN ic.increment_value >= 0 + THEN + CASE ic.system_type_name + WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE 999 + END + ELSE --ic.increment_value is negative + CASE ic.system_type_name + WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE -1 + END + END AS NUMERIC(5,1)) AS percent_remaining + ) AS calc1 + WHERE i.index_id IN (1,0) + AND calc1.percent_remaining <= 30 + OPTION (RECOMPILE); + + + RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; + IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) + AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) + BEGIN + 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 72 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + 'Columnstore Indexes with Trace Flag 834' AS finding, + [database_name] AS [Database Name], + N'https://support.microsoft.com/en-us/kb/3210239' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_type IN (5,6) + OPTION ( RECOMPILE ); + END; + + ---------------------------------------- + --Statistics Info: Check_id 90-99, as well as 125 + ---------------------------------------- + + RAISERROR(N'check_id 90: Outdated 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 ) + SELECT 90 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics Not Updated Recently', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Statistics on this table were last updated ' + + CASE WHEN s.last_statistics_update IS NULL THEN N' NEVER ' + ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + + ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + + ' modifications in that time, which is ' + + CONVERT(NVARCHAR(100), s.percent_modifications) + + '% of the table.' + END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 30) + AND s.percent_modifications >= 10. + AND s.rows >= 10000 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Low Sampling Rates', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows BETWEEN 10000 AND 1000000 AND s.percent_sampled < 10) + 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 ) + SELECT 92 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics With NO RECOMPUTE', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.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 ) + SELECT 94 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Check Constraint with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/computedscalar' AS URL, + 'The check constraint ' + QUOTENAME(cc.constraint_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #CheckConstraints AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 99: Computed Columns 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 ) + SELECT 99 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Computed Column with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/serialudf' AS URL, + 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #ComputedColumns AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + + + END /* IF @Mode IN (0, 4) DIAGNOSE priorities 1-100 */ + + + + + + + + + IF @Mode = 4 /* DIAGNOSE*/ + BEGIN; + RAISERROR(N'@Mode=4, running rules for priorities 101+.', 0,1) WITH NOWAIT; + + RAISERROR(N'check_id 21: More Than 5 Percent NC Indexes Are Unused', 0,1) WITH NOWAIT; + DECLARE @percent_NC_indexes_unused NUMERIC(29,1); + DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); + + SELECT @percent_NC_indexes_unused = ( 100.00 * SUM(CASE + WHEN total_reads = 0 + THEN 1 + ELSE 0 + END) ) / COUNT(*), + @NC_indexes_unused_reserved_MB = SUM(CASE + WHEN total_reads = 0 + THEN sz.total_reserved_MB + ELSE 0 + END) + FROM #IndexSanity i + 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()) + 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, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 21 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'More Than 5 Percent NC Indexes Are Unused' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + + N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, + i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, + '' AS secret_columns, + CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' + + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, + + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + 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()) + GROUP BY i.database_name + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Approximate: Wide Indexes (7 or More Columns)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' + + i.db_schema_object_indexid AS details, i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE ( count_key_columns + count_included_columns ) >= 7 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT database_id, [object_id], + SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + AND key_ordinal > 0 + GROUP BY database_id, object_id + ) + 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 24 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Wide Clustered Index (> 3 columns OR > 16 bytes)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' + + CAST(cc.sum_max_length AS NVARCHAR(10)) + + N' bytes in clustered index:' + i.db_schema_object_name + + N'. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND i.database_id = cc.database_id + WHERE index_id = 1 /* clustered only */ + AND + (count_key_columns > 3 /*More than three key columns.*/ + OR cc.sum_max_length > 16 /*More than 16 bytes in key */) + AND i.is_CX_columnstore = 0 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 25: High ratio of nullable columns.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) AS non_nullable_columns, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 25 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Over-Indexing' AS findings_group, + N'High Ratio of Nulls' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) + + N' of ' + CAST(total_columns AS NVARCHAR(10)) + + N' columns.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = ip.database_id + AND cc.[schema_name] = ip.[schema_name] + WHERE i.index_id IN (1,0) + AND cc.non_nullable_columns < 2 + AND cc.total_columns > 3 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE max_length WHEN -1 THEN 1 ELSE 0 END) AS count_lob_columns, + SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 26 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Wide Tables: 35+ cols or > 2000 non-LOB bytes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' has ' + CAST((total_columns) AS NVARCHAR(10)) + + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) + + N' bytes.' + + CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) AS NVARCHAR(10)) + + ' columns are LOB types.' ELSE '' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = i.database_id + AND cc.[schema_name] = i.[schema_name] + WHERE i.index_id IN (1,0) + AND + (cc.total_columns >= 35 OR + cc.sum_max_length >= 2000) + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 27: High Ratio of Strings.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE WHEN system_type_name IN ('varchar','nvarchar','char') OR max_length=-1 THEN 1 ELSE 0 END) AS string_or_LOB_columns, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 27 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Over-Indexing' AS findings_group, + N'High Ratio of Strings' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) + + N' of ' + CAST(total_columns AS NVARCHAR(10)) + + N' columns. Check if data types are valid.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = i.database_id + AND cc.[schema_name] = i.[schema_name] + CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 + WHERE i.index_id IN (1,0) + AND calc1.non_string_or_lob_columns <= 1 + AND cc.total_columns > 3 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Non-Unique Clustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name + + N' and all NC indexes. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id = 1 /* clustered only */ + AND is_unique=0 /* not unique */ + AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 29: NC indexes with 0 reads and < 10,000 writes', 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 29 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC index with Low Writes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'0 reads: ' + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.user_updates < 10000 + 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()) + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + + + ---------------------------------------- + --Feature-Phobic Indexes: Check_id 30-39 + ---------------------------------------- + RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; + /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 + */ + + SELECT database_name, + SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, + 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes + INTO #index_includes + FROM #IndexSanity + WHERE is_hypothetical = 0 + AND is_disabled = 0 + AND NOT (@GetAllDatabases = 1 OR @Mode = 0) + GROUP BY database_name; + + 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 30 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + database_name AS [Database Name], + N'No Indexes Use Includes' AS finding, 'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'No Indexes Use Includes' AS details, + database_name + N' (Entire database)' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #index_includes + WHERE number_indexes_with_includes = 0 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 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 31 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Few Indexes Use Includes' AS findings, + database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #index_includes + WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 32: filtered indexes and indexed views', 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 DISTINCT + 32 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'No Filtered Indexes or Indexed Views' AS finding, + i.database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'These are NOT always needed-- but do you know when you would use them?' AS details, + i.database_name + N' (Entire database)' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #IndexSanity i + WHERE i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE filter_definition <> '' ) + AND i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE is_indexed_view = 1 ) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, + i.index_sanity_id AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Potential Filtered Index (Based on Column Name)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexColumns ic + JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] + AND ic.database_id =i.database_id + AND ic.schema_name = i.schema_name + AND ic.[index_id]=i.[index_id] + AND i.[index_id] > 1 /* non-clustered index */ + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (column_name LIKE 'is%' + OR column_name LIKE '%archive%' + OR column_name LIKE '%active%' + OR column_name LIKE '%flag%') + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Hypothetical Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Hypothetical Index: ' + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + N'' AS index_usage_summary, + N'' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_hypothetical = 1 + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; + --Note: disabled NC indexes will have O rows in #IndexSanitySize! + 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 42 AS check_id, + index_sanity_id, + 150 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Disabled Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Disabled Index:' + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + 'DISABLED' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_disabled = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 49: Heaps with deletes', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) < 1000 * @DaysUptime /* Only alert about indexes with no forwarded fetches - we already alerted about those in check_id 43 */ + AND SUM(leaf_delete_count) > 0) + 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 49 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Deletes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CAST(h.leaf_delete_count AS NVARCHAR(256)) + N' deletes against heap:' + + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END + OPTION ( RECOMPILE ); + + ---------------------------------------- + --Abnormal Psychology : Check_id 60-79 + ---------------------------------------- + RAISERROR(N'check_id 60: XML indexes', 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 60 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'XML Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + N'' AS index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_XML = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 61: Columnstore indexes', 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 61 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + CASE WHEN i.is_NC_columnstore=1 + THEN N'NC Columnstore Index' + ELSE N'Clustered Columnstore Index' + END AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_NC_columnstore = 1 OR i.is_CX_columnstore=1 + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 62: Spatial indexes', 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 62 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Spatial Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_spatial = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 63: Compressed indexes', 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 63 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Compressed Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 64: Partitioned', 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 64 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Partitioned Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.partition_key_column_name IS NOT NULL + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 65: Non-Aligned Partitioned', 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 65 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Non-Aligned Index on a Partitioned Table' AS finding, + i.[database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanity AS iParent ON + i.[object_id]=iParent.[object_id] + AND i.database_id = iParent.database_id + AND i.schema_name = iParent.schema_name + AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ + AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.partition_key_column_name IS NULL + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Created Tables/Indexes (1 week)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid + N' was created on ' + + CONVERT(NVARCHAR(16),i.create_date,121) + + N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Modified Tables/Indexes (2 days)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid + N' was modified on ' + + CONVERT(NVARCHAR(16),i.modify_date,121) + + N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' + AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) + AND /*Exclude recently created tables.*/ + i.create_date < DATEADD(dd,-7,GETDATE()) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + database_id, + schema_name, + COUNT(*) AS column_count + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + AND collation_name <> @collation + GROUP BY [object_id], + database_id, + schema_name + ) + 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 69 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Column Collation Does Not Match Database Collation' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + + N' has ' + CAST(column_count AS NVARCHAR(20)) + + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END + + N' with a different collation than the db collation of ' + + @collation AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = i.database_id + AND cc.schema_name = i.schema_name + WHERE i.index_id IN (1,0) + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + database_id, + schema_name, + COUNT(*) AS column_count, + SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY object_id, + database_id, + schema_name + ) + 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 70 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Replicated Columns' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) + + N' out of ' + CAST(column_count AS NVARCHAR(20)) + + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END + + N' in one or more publications.' + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND i.database_id = cc.database_id + AND i.schema_name = cc.schema_name + WHERE i.index_id IN (1,0) + AND replicated_column_count > 0 + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 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, more_info ) + SELECT 71 AS check_id, + NULL AS index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Cascading Updates or Deletes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + + N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' + + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' + + N' has settings:' + + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END + + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END + AS details, + [fk].[database_name] AS index_definition, + N'N/A' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary, + (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) + AS more_info + FROM #ForeignKeys fk + WHERE ([delete_referential_action_desc] <> N'NO_ACTION' + OR [update_referential_action_desc] <> N'NO_ACTION') + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 72: Unindexed foreign keys.', 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, more_info ) + SELECT 72 AS check_id, + NULL AS index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Unindexed Foreign Keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + + N' on ' + QUOTENAME(parent_object_name) + N'' + + N' referencing ' + QUOTENAME(referenced_object_name) + N'' + + N' does not appear to have a supporting index.' AS details, + N'N/A' AS index_definition, + N'N/A' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary, + (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) + AS more_info + FROM #UnindexedForeignKeys AS fk + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 73: In-Memory OLTP', 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 73 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'In-Memory OLTP' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.is_in_memory_oltp = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 74: Identity column with unusual seed', 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 74 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Using a Negative Seed or Increment Other Than 1' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE i.index_id IN (1,0) + AND (ic.seed_value < 0 OR ic.increment_value <> 1) + ORDER BY finding, details DESC + OPTION ( RECOMPILE ); + + ---------------------------------------- + --Workaholics: Check_id 80-89 + ---------------------------------------- + + RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 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 ) + + --Workaholics according to index_usage_stats + --This isn't perfect: it mentions the number of scans present in a plan + --A "scan" isn't necessarily a full scan, but hey, we gotta do the best with what we've got. + --in the case of things like indexed views, the operator might be in the plan but never executed + SELECT TOP 5 + 80 AS check_id, + i.index_sanity_id AS index_sanity_id, + 200 AS Priority, + N'High Workloads' AS findings_group, + N'Scan-a-lots (index-usage-stats)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/Workaholics' AS URL, + REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') + + N' scans against ' + i.db_schema_object_indexid + + N'. Latest scan: ' + ISNULL(CAST(i.last_user_scan AS NVARCHAR(128)),'?') + N'. ' + + N'ScanFactor=' + CAST(((i.user_scans * iss.total_reserved_MB)/1000000.) AS NVARCHAR(256)) AS details, + ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, + ISNULL(i.secret_columns,'') AS secret_columns, + i.index_usage_summary AS index_usage_summary, + iss.index_size_summary AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id + WHERE ISNULL(i.user_scans,0) > 0 + ORDER BY i.user_scans * iss.total_reserved_MB DESC + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 81: Top recent accesses (op stats)', 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 ) + --Workaholics according to index_operational_stats + --This isn't perfect either: range_scan_count contains full scans, partial scans, even seeks in nested loop ops + --But this can help bubble up some most-accessed tables + SELECT TOP 5 + 81 AS check_id, + i.index_sanity_id AS index_sanity_id, + 200 AS Priority, + N'High Workloads' AS findings_group, + N'Top Recent Accesses (index-op-stats)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/Workaholics' AS URL, + ISNULL(REPLACE( + CONVERT(NVARCHAR(50),CAST((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), + N'.00',N'') + + N' uses of ' + i.db_schema_object_indexid + N'. ' + + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_range_scan_count AS MONEY),1),N'.00',N'') + N' scans or seeks. ' + + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups. ' + + N'OpStatsFactor=' + CAST(((((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB))/1000000.) AS VARCHAR(256)),'') AS details, + ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, + ISNULL(i.secret_columns,'') AS secret_columns, + i.index_usage_summary AS index_usage_summary, + iss.index_size_summary AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id + WHERE (ISNULL(iss.total_range_scan_count,0) > 0 OR ISNULL(iss.total_singleton_lookup_count,0) > 0) + ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC + OPTION ( RECOMPILE ); + + + + RAISERROR(N'check_id 93: Statistics with filters', 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 93 AS check_id, + 200 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics With Filters', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'The statistic ' + QUOTENAME(s.statistics_name) + ' is filtered on [' + s.filter_definition + ']. It could be part of a filtered index, or just a filtered statistic. This is purely informational.' 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_filter = 1 + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 100: Computed Columns that are not Persisted.', 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 100 AS check_id, + 200 AS Priority, + 'Repeated Calculations' AS findings_group, + 'Computed Columns Not Persisted' AS finding, + cc.database_name, + '' AS URL, + 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is not persisted, which means it will be calculated when a query runs.' + + 'You can change this with the following command, if the definition is deterministic: ALTER TABLE ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' ALTER COLUMN ' + cc.column_name + + ' ADD PERSISTED' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #ComputedColumns AS cc + WHERE cc.is_persisted = 0 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 110: Temporal Tables.', 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 110 AS check_id, + 200 AS Priority, + 'Abnormal Design Pattern' AS findings_group, + 'Temporal Tables', + t.database_name, + '' AS URL, + 'The table ' + QUOTENAME(t.schema_name) + '.' + QUOTENAME(t.table_name) + ' is a temporal table, with rows versioned in ' + + QUOTENAME(t.history_schema_name) + '.' + QUOTENAME(t.history_table_name) + ' on History columns ' + QUOTENAME(t.start_column_name) + ' and ' + QUOTENAME(t.end_column_name) + '.' + AS details, + '' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #TemporalTables AS t + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 121: Optimized For Sequential Keys.', 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 121 AS check_id, + 200 AS Priority, + 'Specialized Indexes' AS findings_group, + 'Optimized For Sequential Keys', + i.database_name, + '' AS URL, + 'The table ' + QUOTENAME(i.schema_name) + '.' + QUOTENAME(i.object_name) + ' is optimized for sequential keys.' AS details, + '' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #IndexSanity AS i + WHERE i.optimize_for_sequential_key = 1 + 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 */ + + + + + + + + + RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; + IF DATEDIFF(MM, @VersionDate, GETDATE()) > 6 + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, 0 , + 'Outdated sp_BlitzIndex', 'sp_BlitzIndex is Over 6 Months Old', 'http://FirstResponderKit.org/', + 'Fine wine gets better with age, but this ' + @ScriptVersionName + ' is more like bad cheese. Time to get a new one.', + @DaysUptimeInsertValue,N'',N'' + ); + END; + + IF EXISTS(SELECT * FROM #BlitzIndexResults) + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue,N'',N'' + ); + END; + ELSE IF @Mode = 0 OR (@GetAllDatabases = 1 AND @Mode <> 4) + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, N'',N'' + ); + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, 0 , + N'No Major Problems Found', + N'Nice Work!', + N'http://FirstResponderKit.org', + N'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', + N'The default Mode 0 only looks for very serious index issues.', + @DaysUptimeInsertValue, N'' + ); + + END; + ELSE + BEGIN + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( -1, 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, N'',N'' + ); + INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) + VALUES ( 1, 0 , + N'No Problems Found', + N'Nice job! Or more likely, you have a nearly empty database.', + N'http://FirstResponderKit.org', 'Time to go read some blog posts.', + @DaysUptimeInsertValue, N'', N'' + ); + + END; + + RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; + + /*Return results.*/ + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [priority] INT, + [finding] NVARCHAR(4000), + [database_name] NVARCHAR(128), + [details] NVARCHAR(MAX), + [index_definition] NVARCHAR(MAX), + [secret_columns] NVARCHAR(MAX), + [index_usage_summary] NVARCHAR(MAX), + [index_size_summary] NVARCHAR(MAX), + [more_info] NVARCHAR(MAX), + [url] NVARCHAR(MAX), + [create_tsql] NVARCHAR(MAX), + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [priority], + [finding], + [database_name], + [details], + [index_definition], + [secret_columns], + [index_usage_summary], + [index_size_summary], + [more_info], + [url], + [create_tsql], + [sample_query_plan] + ) + SELECT + ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + Priority, ISNULL(br.findings_group,N'''') + + CASE WHEN ISNULL(br.finding,N'''') <> N'''' THEN N'': '' ELSE N'''' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'''') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'''') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'''') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + SELECT Priority, ISNULL(br.findings_group,N'') + + CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE); + END; + + END; + + END /* End @Mode=0 or 4 (diagnose)*/ + + + + + + + + + ELSE IF (@Mode=1) /*Summarize*/ + BEGIN + --This mode is to give some overall stats on the database. + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [object_count] INT, + [reserved_gb] NUMERIC(29,1), + [reserved_lob_gb] NUMERIC(29,1), + [reserved_row_overflow_gb] NUMERIC(29,1), + [clustered_table_count] INT, + [clustered_table_gb] NUMERIC(29,1), + [nc_index_count] INT, + [nc_index_gb] NUMERIC(29,1), + [table_nc_index_ratio] NUMERIC(29,1), + [heap_count] INT, + [heap_gb] NUMERIC(29,1), + [partitioned_table_count] INT, + [partitioned_nc_count] INT, + [partitioned_gb] NUMERIC(29,1), + [filtered_index_count] INT, + [indexed_view_count] INT, + [max_table_row_count] INT, + [max_table_gb] NUMERIC(29,1), + [max_nc_index_gb] NUMERIC(29,1), + [table_count_over_1gb] INT, + [table_count_over_10gb] INT, + [table_count_over_100gb] INT, + [nc_index_count_over_1gb] INT, + [nc_index_count_over_10gb] INT, + [nc_index_count_over_100gb] INT, + [min_create_date] DATETIME, + [max_create_date] DATETIME, + [max_modify_date] DATETIME, + [display_order] INT, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [object_count], + [reserved_gb], + [reserved_lob_gb], + [reserved_row_overflow_gb], + [clustered_table_count], + [clustered_table_gb], + [nc_index_count], + [nc_index_gb], + [table_nc_index_ratio], + [heap_count], + [heap_gb], + [partitioned_table_count], + [partitioned_nc_count], + [partitioned_gb], + [filtered_index_count], + [indexed_view_count], + [max_table_row_count], + [max_table_gb], + [max_nc_index_gb], + [table_count_over_1gb], + [table_count_over_10gb], + [table_count_over_100gb], + [nc_index_count_over_1gb], + [nc_index_count_over_10gb], + [nc_index_count_over_100gb], + [min_create_date], + [max_create_date], + [max_modify_date], + [display_order] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! initial columns are not casted to nvarchar due to not outputing informational line + DB_NAME(i.database_id) AS [Database Name], + COUNT(*) AS [Number Objects], + CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS [All GB], + CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS [LOB GB], + CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS [Row Overflow GB], + SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '''' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don''t lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + ORDER BY [Display Order] ASC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END; /* @ValidOutputLocation = 1 */ + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + + RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; + + SELECT DB_NAME(i.database_id) AS [Database Name], + CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], + CAST(CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], + CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], + CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], + CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + UNION ALL + SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,0 AS display_order + ORDER BY [Display Order] ASC + OPTION (RECOMPILE); + END; + END; + + END; /* End @Mode=1 (summarize)*/ + + + + + + + + + ELSE IF (@Mode=2) /*Index Detail*/ + BEGIN + --This mode just spits out all the detail without filters. + --This supports slicing AND dicing in Excel + RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; + + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + IF @SchemaExists = 1 + BEGIN + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [schema_name] NVARCHAR(128), + [table_name] NVARCHAR(128), + [index_name] NVARCHAR(128), + [Drop_Tsql] NVARCHAR(MAX), + [Create_Tsql] NVARCHAR(MAX), + [index_id] INT, + [db_schema_object_indexid] NVARCHAR(500), + [object_type] NVARCHAR(15), + [index_definition] NVARCHAR(MAX), + [key_column_names_with_sort_order] NVARCHAR(MAX), + [count_key_columns] INT, + [include_column_names] NVARCHAR(MAX), + [count_included_columns] INT, + [secret_columns] NVARCHAR(MAX), + [count_secret_columns] INT, + [partition_key_column_name] NVARCHAR(MAX), + [filter_definition] NVARCHAR(MAX), + [is_indexed_view] BIT, + [is_primary_key] BIT, + [is_unique_constraint] BIT, + [is_XML] BIT, + [is_spatial] BIT, + [is_NC_columnstore] BIT, + [is_CX_columnstore] BIT, + [is_in_memory_oltp] BIT, + [is_disabled] BIT, + [is_hypothetical] BIT, + [is_padded] BIT, + [fill_factor] INT, + [is_referenced_by_foreign_key] BIT, + [last_user_seek] DATETIME, + [last_user_scan] DATETIME, + [last_user_lookup] DATETIME, + [last_user_update] DATETIME, + [total_reads] BIGINT, + [user_updates] BIGINT, + [reads_per_write] MONEY, + [index_usage_summary] NVARCHAR(200), + [total_singleton_lookup_count] BIGINT, + [total_range_scan_count] BIGINT, + [total_leaf_delete_count] BIGINT, + [total_leaf_update_count] BIGINT, + [index_op_stats] NVARCHAR(200), + [partition_count] INT, + [total_rows] BIGINT, + [total_reserved_MB] NUMERIC(29,2), + [total_reserved_LOB_MB] NUMERIC(29,2), + [total_reserved_row_overflow_MB] NUMERIC(29,2), + [index_size_summary] NVARCHAR(300), + [total_row_lock_count] BIGINT, + [total_row_lock_wait_count] BIGINT, + [total_row_lock_wait_in_ms] BIGINT, + [avg_row_lock_wait_in_ms] BIGINT, + [total_page_lock_count] BIGINT, + [total_page_lock_wait_count] BIGINT, + [total_page_lock_wait_in_ms] BIGINT, + [avg_page_lock_wait_in_ms] BIGINT, + [total_index_lock_promotion_attempt_count] BIGINT, + [total_index_lock_promotion_count] BIGINT, + [total_forwarded_fetch_count] BIGINT, + [data_compression_desc] NVARCHAR(4000), + [page_latch_wait_count] BIGINT, + [page_latch_wait_in_ms] BIGINT, + [page_io_latch_wait_count] BIGINT, + [page_io_latch_wait_in_ms] BIGINT, + [create_date] DATETIME, + [modify_date] DATETIME, + [more_info] NVARCHAR(500), + [display_order] INT, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF @TableExists = 1 + BEGIN + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [schema_name], + [table_name], + [index_name], + [Drop_Tsql], + [Create_Tsql], + [index_id], + [db_schema_object_indexid], + [object_type], + [index_definition], + [key_column_names_with_sort_order], + [count_key_columns], + [include_column_names], + [count_included_columns], + [secret_columns], + [count_secret_columns], + [partition_key_column_name], + [filter_definition], + [is_indexed_view], + [is_primary_key], + [is_unique_constraint], + [is_XML], + [is_spatial], + [is_NC_columnstore], + [is_CX_columnstore], + [is_in_memory_oltp], + [is_disabled], + [is_hypothetical], + [is_padded], + [fill_factor], + [is_referenced_by_foreign_key], + [last_user_seek], + [last_user_scan], + [last_user_lookup], + [last_user_update], + [total_reads], + [user_updates], + [reads_per_write], + [index_usage_summary], + [total_singleton_lookup_count], + [total_range_scan_count], + [total_leaf_delete_count], + [total_leaf_update_count], + [index_op_stats], + [partition_count], + [total_rows], + [total_reserved_MB], + [total_reserved_LOB_MB], + [total_reserved_row_overflow_MB], + [index_size_summary], + [total_row_lock_count], + [total_row_lock_wait_count], + [total_row_lock_wait_in_ms], + [avg_row_lock_wait_in_ms], + [total_page_lock_count], + [total_page_lock_wait_count], + [total_page_lock_wait_in_ms], + [avg_page_lock_wait_in_ms], + [total_index_lock_promotion_attempt_count], + [total_index_lock_promotion_count], + [total_forwarded_fetch_count], + [data_compression_desc], + [page_latch_wait_count], + [page_latch_wait_in_ms], + [page_io_latch_wait_count], + [page_io_latch_wait_in_ms], + [create_date], + [modify_date], + [more_info], + [display_order] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + i.[database_name] AS [Database Name], + i.[schema_name] AS [Schema Name], + i.[object_name] AS [Object Name], + ISNULL(i.index_name, '''') AS [Index Name], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.index_definition <> ''[HEAP]'' + THEN N''--DROP INDEX ''+ QUOTENAME(i.index_name) + N'' ON '' + QUOTENAME(i.[database_name]) + N''.'' + + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + N'';'' + ELSE N'''' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = ''[HEAP]'' THEN N'''' + ELSE N''--'' + ict.create_tsql END AS [Create TSQL], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], + db_schema_object_indexid AS [Details: schema.table.index(indexid)], + CASE WHEN index_id IN ( 1, 0 ) THEN ''TABLE'' + ELSE ''NonClustered'' + END AS [Object Type], + LEFT(index_definition,4000) AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(LTRIM(key_column_names_with_sort_order), '''') AS [Key Column Names With Sort], + ISNULL(count_key_columns, 0) AS [Count Key Columns], + ISNULL(include_column_names, '''') AS [Include Column Names], + ISNULL(count_included_columns,0) AS [Count Included Columns], + ISNULL(secret_columns,'''') AS [Secret Column Names], + ISNULL(count_secret_columns,0) AS [Count Secret Columns], + ISNULL(partition_key_column_name, '''') AS [Partition Key Column Name], + ISNULL(filter_definition, '''') AS [Filter Definition], + is_indexed_view AS [Is Indexed View], + is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint], + is_XML AS [Is XML], + is_spatial AS [Is Spatial], + is_NC_columnstore AS [Is NC Columnstore], + is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], + is_disabled AS [Is Disabled], + is_hypothetical AS [Is Hypothetical], + is_padded AS [Is Padded], + fill_factor AS [Fill Factor], + is_referenced_by_foreign_key AS [Is Reference by Foreign Key], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Update], + total_reads AS [Total Reads], + user_updates AS [User Updates], + reads_per_write AS [Reads Per Write], + index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], + sz.partition_count AS [Partition Count], + sz.total_rows AS [Rows], + sz.total_reserved_MB AS [Reserved MB], + sz.total_reserved_LOB_MB AS [Reserved LOB MB], + sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], + sz.index_size_summary AS [Index Size], + sz.total_row_lock_count AS [Row Lock Count], + sz.total_row_lock_wait_count AS [Row Lock Wait Count], + sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], + sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], + sz.total_page_lock_count AS [Page Lock Count], + sz.total_page_lock_wait_count AS [Page Lock Wait Count], + sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], + sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], + sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], + sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], + sz.data_compression_desc AS [Data Compression], + sz.page_latch_wait_count, + sz.page_latch_wait_in_ms, + sz.page_io_latch_wait_count, + sz.page_io_latch_wait_in_ms, + i.create_date AS [Create Date], + i.modify_date AS [Modify Date], + more_info AS [More Info], + 1 AS [Display Order] + FROM #IndexSanity AS i + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id + ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + END; /* @TableExists = 1 */ + ELSE + RAISERROR('Creation of the output table failed.', 16, 0); + END; /* @TableExists = 0 */ + ELSE + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + END; /* @ValidOutputLocation = 1 */ + ELSE + + IF(@OutputType <> 'NONE') + BEGIN + SELECT i.[database_name] AS [Database Name], + i.[schema_name] AS [Schema Name], + i.[object_name] AS [Object Name], + ISNULL(i.index_name, '') AS [Index Name], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], + db_schema_object_indexid AS [Details: schema.table.index(indexid)], + CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' + ELSE 'NonClustered' + END AS [Object Type], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], + ISNULL(count_key_columns, 0) AS [Count Key Columns], + ISNULL(include_column_names, '') AS [Include Column Names], + ISNULL(count_included_columns,0) AS [Count Included Columns], + ISNULL(secret_columns,'') AS [Secret Column Names], + ISNULL(count_secret_columns,0) AS [Count Secret Columns], + ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], + ISNULL(filter_definition, '') AS [Filter Definition], + is_indexed_view AS [Is Indexed View], + is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint] , + is_XML AS [Is XML], + is_spatial AS [Is Spatial], + is_NC_columnstore AS [Is NC Columnstore], + is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], + is_disabled AS [Is Disabled], + is_hypothetical AS [Is Hypothetical], + is_padded AS [Is Padded], + fill_factor AS [Fill Factor], + is_referenced_by_foreign_key AS [Is Reference by Foreign Key], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Update], + total_reads AS [Total Reads], + user_updates AS [User Updates], + reads_per_write AS [Reads Per Write], + index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], + sz.partition_count AS [Partition Count], + sz.total_rows AS [Rows], + sz.total_reserved_MB AS [Reserved MB], + sz.total_reserved_LOB_MB AS [Reserved LOB MB], + sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], + sz.index_size_summary AS [Index Size], + sz.total_row_lock_count AS [Row Lock Count], + sz.total_row_lock_wait_count AS [Row Lock Wait Count], + sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], + sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], + sz.total_page_lock_count AS [Page Lock Count], + sz.total_page_lock_wait_count AS [Page Lock Wait Count], + sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], + sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], + sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], + sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.page_latch_wait_count AS [Page Latch Wait Count], + sz.page_latch_wait_in_ms AS [Page Latch Wait ms], + sz.page_io_latch_wait_count AS [Page IO Latch Wait Count], + sz.page_io_latch_wait_in_ms as [Page IO Latch Wait ms], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], + sz.data_compression_desc AS [Data Compression], + i.create_date AS [Create Date], + i.modify_date AS [Modify Date], + more_info AS [More Info], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(i.index_name) + N' ON ' + QUOTENAME(i.[database_name]) + N'.' + + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + N';' + ELSE N'' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = '[HEAP]' THEN N'' + ELSE N'--' + ict.create_tsql END AS [Create TSQL], + 1 AS [Display Order] + INTO #Mode2Temp + FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id + OPTION(RECOMPILE); + + IF @@ROWCOUNT > 0 + BEGIN + SELECT + sz.* + FROM #Mode2Temp AS sz + ORDER BY /* Shout out to DHutmacher */ + /*DESC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'desc' THEN sz.[Rows] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'desc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END DESC, + /*ASC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'asc' THEN sz.[Rows] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'asc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END ASC, + sz.[Database Name], [Schema Name], [Object Name], [Index ID] + OPTION (RECOMPILE); + END + ELSE + BEGIN + SELECT + DatabaseDetails = + N'Database ' + + ISNULL(@DatabaseName, DB_NAME()) + + N' has ' + + ISNULL(RTRIM(@Rowcount), 0) + + N' partitions.', + BringThePain = + CASE + WHEN @BringThePain IN (0, 1) AND ISNULL(@Rowcount, 0) = 0 + THEN N'Check the database name, it looks like nothing is here.' + WHEN @BringThePain = 0 AND ISNULL(@Rowcount, 0) > 0 + THEN N'Please re-run with @BringThePain = 1' + END; + END + END; + END; /* End @Mode=2 (index detail)*/ + + + + + + + + + ELSE IF (@Mode=3) /*Missing index Detail*/ + BEGIN + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [schema_name] NVARCHAR(128), + [table_name] NVARCHAR(128), + [magic_benefit_number] BIGINT, + [missing_index_details] NVARCHAR(MAX), + [avg_total_user_cost] NUMERIC(29,4), + [avg_user_impact] NUMERIC(29,1), + [user_seeks] BIGINT, + [user_scans] BIGINT, + [unique_compiles] BIGINT, + [equality_columns_with_data_type] NVARCHAR(MAX), + [inequality_columns_with_data_type] NVARCHAR(MAX), + [included_columns_with_data_type] NVARCHAR(MAX), + [index_estimated_impact] NVARCHAR(256), + [create_tsql] NVARCHAR(MAX), + [more_info] NVARCHAR(600), + [display_order] INT, + [is_low] BIT, + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + SET @StringToExecute = + N'WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [schema_name], + [table_name], + [magic_benefit_number], + [missing_index_details], + [avg_total_user_cost], + [avg_user_impact], + [user_seeks], + [user_scans], + [unique_compiles], + [equality_columns_with_data_type], + [inequality_columns_with_data_type], + [included_columns_with_data_type], + [index_estimated_impact], + [create_tsql], + [more_info], + [display_order], + [is_low], + [sample_query_plan] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! CTE block is above insert in the copied SQL + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC sp_executesql @StringToExecute, N'@DaysUptime NUMERIC(23,2), @ShowAllMissingIndexRequests BIT', @DaysUptime = @DaysUptime, @ShowAllMissingIndexRequests = @ShowAllMissingIndexRequests; + + END; /* @ValidOutputLocation = 1 */ + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + SELECT + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + UNION ALL + SELECT + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + 100000000000, + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL, NULL, NULL, NULL, 0 AS [Display Order], NULL AS is_low, NULL + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE); + END; + + + IF (@BringThePain = 1 + AND @DatabaseName IS NOT NULL + AND @GetAllDatabases = 0) + + BEGIN + EXEC sp_BlitzCache @SortOrder = 'sp_BlitzIndex', @DatabaseName = @DatabaseName, @BringThePain = 1, @QueryFilter = 'statement', @HideSummary = 1; + END; + + END; + + + + + + END; /* End @Mode=3 (index detail)*/ + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'finishing at %s',0,1, @d) WITH NOWAIT; +END /* End @TableName IS NULL (mode 0/1/2/3/4) */ +END TRY + +BEGIN CATCH + RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; + + SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + + RAISERROR (@msg, + @ErrorSeverity, + @ErrorState + ); + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; + END CATCH; +GO +IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL +BEGIN + EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); +END; +GO + +ALTER PROCEDURE + dbo.sp_BlitzLock +( + @DatabaseName sysname = NULL, + @StartDate datetime = NULL, + @EndDate datetime = NULL, + @ObjectName nvarchar(1024) = NULL, + @StoredProcName nvarchar(1024) = NULL, + @AppName sysname = NULL, + @HostName sysname = NULL, + @LoginName sysname = NULL, + @EventSessionName sysname = N'system_health', + @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, + @VersionDate datetime = NULL OUTPUT, + @VersionCheckMode bit = 0, + @OutputDatabaseName sysname = NULL, + @OutputSchemaName sysname = N'dbo', /*ditto as below*/ + @OutputTableName sysname = N'BlitzLock', /*put a standard here no need to check later in the script*/ + @ExportToExcel bit = 0 +) +WITH RECOMPILE +AS +BEGIN + SET STATISTICS XML OFF; + SET NOCOUNT ON; + SET XACT_ABORT OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF @VersionCheckMode = 1 + BEGIN + RETURN; + END; + + IF @Help = 1 + BEGIN + PRINT N' + /* + sp_BlitzLock from http://FirstResponderKit.org + + This script checks for and analyzes deadlocks from the system health session or a custom extended event path + + 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 + + @EndDate: The date you want to stop searching on, defaults to current date + + @ObjectName: If you want to filter to a specific able. + The object name has to be fully qualified ''Database.Schema.Table'' + + @StoredProcName: If you want to search for a single stored proc + The proc name has to be fully qualified ''Database.Schema.Sproc'' + + @AppName: If you want to filter to a specific application + + @HostName: If you want to filter to a specific host + + @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'', ''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. + + Known limitations of this version: + - Only SQL Server 2012 and newer is supported + - If your tables have weird characters in them (https://en.wikipedia.org/wiki/List_of_xml_and_HTML_character_entity_references) you may get errors trying to parse the xml. + I took a long look at this one, and: + 1) Trying to account for all the weird places these could crop up is a losing effort. + 2) Replace is slow af on lots of xml. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) + + MIT License + + Copyright (c) Brent Ozar Unlimited + + 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. + + */'; + + RETURN; + END; /* @Help = 1 */ + + /*Declare local variables used in the procudure*/ + DECLARE + @DatabaseId int = + DB_ID(@DatabaseName), + @ProductVersion nvarchar(128) = + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + @ProductVersionMajor float = + SUBSTRING + ( + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + 1, + CHARINDEX('.', CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128))) + 1 + ), + @ProductVersionMinor int = + PARSENAME + ( + CONVERT + ( + varchar(32), + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) + ), + 2 + ), + @ObjectFullName nvarchar(MAX) = N'', + @Azure bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 5 + THEN 1 + ELSE 0 + END, + @MI bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 8 + THEN 1 + ELSE 0 + END, + @RDS bit = + CASE + WHEN LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND DB_ID('rdsadmin') IS NULL + THEN 0 + ELSE 1 + END, + @d varchar(40) = '', + @StringToExecute nvarchar(4000) = N'', + @StringToExecuteParams nvarchar(500) = N'', + @r sysname = NULL, + @OutputTableFindings nvarchar(100) = N'[BlitzLockFindings]', + @DeadlockCount int = 0, + @ServerName sysname = @@SERVERNAME, + @OutputDatabaseCheck bit = -1, + @SessionId int = 0, + @TargetSessionId int = 0, + @FileName nvarchar(4000) = N'', + @inputbuf_bom nvarchar(1) = CONVERT(nvarchar(1), 0x0a00, 0), + @deadlock_result nvarchar(MAX) = N'', + @StartDateOriginal datetime = @StartDate, + @EndDateOriginal datetime = @EndDate, + @StartDateUTC datetime, + @EndDateUTC datetime, + @extract_sql nvarchar(MAX), + @validation_sql nvarchar(MAX), + @xe bit, + @xd bit; + + /*Temporary objects used in the procedure*/ + DECLARE + @sysAssObjId AS table + ( + database_id int, + partition_id bigint, + schema_name sysname, + table_name sysname + ); + + CREATE TABLE + #x + ( + x xml NOT NULL + DEFAULT N'x' + ); + + CREATE TABLE + #deadlock_data + ( + deadlock_xml xml NOT NULL + DEFAULT N'x' + ); + + CREATE TABLE + #t + ( + id int NOT NULL + ); + + CREATE TABLE + #deadlock_findings + ( + id int IDENTITY PRIMARY KEY, + check_id int NOT NULL, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000), + sort_order bigint + ); + + /*Set these to some sane defaults if NULLs are passed in*/ + /*Normally I'd hate this, but we RECOMPILE everything*/ + + SELECT + @StartDate = + CASE + WHEN @StartDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + DATEADD + ( + DAY, + -7, + SYSDATETIME() + ) + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @StartDate + ) + END, + @EndDate = + CASE + WHEN @EndDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + SYSDATETIME() + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @EndDate + ) + END; + + SELECT + @StartDateUTC = @StartDate, + @EndDateUTC = @EndDate; + + IF + ( + @MI = 1 + AND @EventSessionName = N'system_health' + AND @TargetSessionType IS NULL + ) + BEGIN + SET + @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 + ( + SELECT + 1/0 + FROM sys.server_event_sessions AS ses + JOIN sys.dm_xe_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; + + IF @Azure = 1 + AND LOWER(@TargetSessionType) <> N'table' + BEGIN + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.database_event_sessions AS ses + JOIN sys.dm_xe_database_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; + + IF @OutputDatabaseName IS NOT NULL + BEGIN /*IF databaseName is set, do some sanity checks and put [] around def.*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@OutputDatabaseName set to %s, checking validity at %s', 0, 1, @OutputDatabaseName, @d) WITH NOWAIT; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name = @OutputDatabaseName + ) /*If database is invalid raiserror and set bitcheck*/ + BEGIN + RAISERROR('Database Name (%s) for output of table is invalid please, Output to Table will not be performed', 0, 1, @OutputDatabaseName) WITH NOWAIT; + SET @OutputDatabaseCheck = -1; /* -1 invalid/false, 0 = good/true */ + END; + ELSE + BEGIN + SET @OutputDatabaseCheck = 0; + + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + 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 s.name =''' + + @OutputSchemaName + + N''';', + @StringToExecuteParams = + N'@r sysname OUTPUT'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; + + IF @Debug = 1 + BEGIN + RAISERROR('@r is set to: %s for schema name %s and table name %s', 0, 1, @r, @OutputSchemaName, @OutputTableName) WITH NOWAIT; + END; + + /*protection spells*/ + SELECT + @ObjectFullName = + QUOTENAME(@OutputDatabaseName) + + N'.' + + QUOTENAME(@OutputSchemaName) + + N'.' + + QUOTENAME(@OutputTableName), + @OutputDatabaseName = + QUOTENAME(@OutputDatabaseName), + @OutputTableName = + QUOTENAME(@OutputTableName), + @OutputSchemaName = + QUOTENAME(@OutputSchemaName); + + IF (@r IS NOT NULL) /*if it is not null, there is a table, so check for newly added columns*/ + BEGIN + /* If the table doesn't have the new spid column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''spid'') + /*Add spid column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD spid smallint NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new wait_resource column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''wait_resource'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD wait_resource nvarchar(MAX) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_1'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_1 varchar(500) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_2'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_2 varchar(500) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new lock mode column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''lock_mode'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD lock_mode nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new status column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''status'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD status nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + 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 */ + BEGIN + SELECT + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableName + + N' ( + ServerName nvarchar(256), + deadlock_type nvarchar(256), + event_date datetime, + database_name nvarchar(256), + spid smallint, + deadlock_group nvarchar(256), + query xml, + object_names xml, + isolation_level nvarchar(256), + owner_mode nvarchar(256), + waiter_mode nvarchar(256), + lock_mode nvarchar(256), + transaction_count bigint, + client_option_1 varchar(500), + client_option_2 varchar(500), + login_name nvarchar(256), + host_name nvarchar(256), + client_app nvarchar(1024), + wait_time bigint, + wait_resource nvarchar(max), + priority smallint, + log_used bigint, + last_tran_started datetime, + last_batch_started datetime, + last_batch_completed datetime, + transaction_name nvarchar(256), + status nvarchar(256), + owner_waiter_type nvarchar(256), + owner_activity nvarchar(256), + owner_waiter_activity nvarchar(256), + owner_merging nvarchar(256), + owner_spilling nvarchar(256), + owner_waiting_to_close nvarchar(256), + waiter_waiter_type nvarchar(256), + waiter_owner_activity nvarchar(256), + waiter_waiter_activity nvarchar(256), + waiter_merging nvarchar(256), + waiter_spilling nvarchar(256), + waiter_waiting_to_close nvarchar(256), + deadlock_graph xml + )'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*table created.*/ + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + N'.sys.objects AS o + WHERE o.type_desc = N''USER_TABLE'' + AND o.name = N''BlitzLockFindings''', + @StringToExecuteParams = + N'@r sysname OUTPUT'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; + + IF (@r IS NULL) /*if table does not exist*/ + BEGIN + SELECT + @OutputTableFindings = + QUOTENAME(N'BlitzLockFindings'), + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableFindings + + N' ( + ServerName nvarchar(256), + check_id INT, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000) + );'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; + END; + + /*create synonym for deadlockfindings.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadlockFindings' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadlockFindings; + END; + + RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableFindings; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*create synonym for deadlock table.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadLockTbl' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadLockTbl; + END; + + RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableName; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; + END; + + /* WITH ROWCOUNT doesn't work on Amazon RDS - see: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2037 */ + IF @RDS = 0 + BEGIN; + BEGIN TRY; + RAISERROR('@RDS = 0, updating #t with high row and page counts', 0, 1) WITH NOWAIT; + UPDATE STATISTICS + #t + WITH + ROWCOUNT = 9223372036854775807, + PAGECOUNT = 9223372036854775807; + END TRY + BEGIN CATCH; + /* Misleading error returned, if run without permissions to update statistics the error returned is "Cannot find object". + Catching specific error, and returning message with better info. If any other error is returned, then throw as normal */ + IF (ERROR_NUMBER() = 1088) + BEGIN; + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Cannot run UPDATE STATISTICS on a #temp table without db_owner or sysadmin permissions', 0, 1) WITH NOWAIT; + END; + ELSE + BEGIN; + THROW; + END; + END CATCH; + END; + + IF @DeadlockType IS NOT NULL + BEGIN + SELECT + @DeadlockType = + CASE + WHEN LOWER(@DeadlockType) LIKE 'regular%' + THEN N'Regular Deadlock' + WHEN LOWER(@DeadlockType) LIKE N'parallel%' + THEN N'Parallel Deadlock' + ELSE NULL + END; + END; + + /*If @TargetSessionType, we need to figure out if it's ring buffer or event file*/ + /*Azure has differently named views, so we need to separate. Thanks, Azure.*/ + + IF + ( + @Azure = 0 + AND @TargetSessionType IS NULL + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for non-Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_sessions AS s + JOIN sys.dm_xe_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for non-Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; + + IF + ( + @Azure = 1 + AND @TargetSessionType IS NULL + AND LOWER(@TargetSessionType) <> N'table' + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_database_sessions AS s + JOIN sys.dm_xe_database_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; + + + /*The system health stuff gets handled different from user extended events.*/ + /*These next sections deal with user events, dependent on target.*/ + + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for non-Azure at %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_session_targets AS t + JOIN sys.dm_xe_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for Azure at %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_database_session_targets AS t + JOIN sys.dm_xe_database_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + END; + + + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for non-Azure', 0, 1) WITH NOWAIT; + + SELECT + @SessionId = t.event_session_id, + @TargetSessionId = t.target_id + FROM sys.server_event_session_targets AS t + JOIN sys.server_event_sessions AS s + ON s.event_session_id = t.event_session_id + WHERE t.name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; + + IF @Azure = 1 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for Azure', 0, 1) WITH NOWAIT; + SELECT + @SessionId = + t.event_session_address, + @TargetSessionId = + t.target_name + FROM sys.dm_xe_database_session_targets t + JOIN sys.dm_xe_database_sessions s + ON s.address = t.event_session_address + WHERE t.target_name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Reading for event_file %s', 0, 1, @FileName) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(f.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(@FileName, NULL, NULL, NULL) AS f + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*The XML is parsed differently if it comes from the event file or ring buffer*/ + + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for ring buffer data', 0, 1) WITH NOWAIT; + + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query(N'.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.nodes('/RingBufferTarget/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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event_file%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for event file data', 0, 1) WITH NOWAIT; + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query('.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*This section deals with event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Grab the initial set of xml to parse at %s', 0, 1, @d) WITH NOWAIT; + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + SELECT + xml.deadlock_xml + INTO #xml + FROM + ( + SELECT + deadlock_xml = + TRY_CAST(fx.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS fx + LEFT JOIN #t AS t + ON 1 = 1 + WHERE fx.object_name = N'xml_deadlock_report' + ) AS xml + CROSS APPLY xml.deadlock_xml.nodes('/event') AS e(x) + WHERE 1 = 1 + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + INSERT + #deadlock_data WITH(TABLOCKX) + SELECT + deadlock_xml = + xml.deadlock_xml + FROM #xml AS xml + LEFT JOIN #t AS t + ON 1 = 1 + WHERE xml.deadlock_xml IS NOT NULL + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /* If table target */ + 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; + + /* + 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 ' + + 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); + '; + + 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; + + SELECT + d1.deadlock_xml, + event_date = d1.deadlock_xml.value('(event/@timestamp)[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('/event/data/value/deadlock') + INTO #dd + FROM #deadlock_data AS d1 + LEFT JOIN #t AS t + ON 1 = 1 + WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' + + 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); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Final Parse process and input buffer xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT + q.event_date, + q.victim_id, + is_parallel = + CONVERT(bit, q.is_parallel), + q.deadlock_graph, + q.id, + q.spid, + q.database_id, + database_name = + ISNULL + ( + DB_NAME(q.database_id), + N'UNKNOWN' + ), + q.current_database_name, + q.priority, + q.log_used, + q.wait_resource, + q.wait_time, + q.transaction_name, + q.last_tran_started, + q.last_batch_started, + q.last_batch_completed, + q.lock_mode, + q.status, + q.transaction_count, + q.client_app, + q.host_name, + q.login_name, + q.isolation_level, + client_option_1 = + SUBSTRING + ( + CASE WHEN q.clientoption1 & 1 = 1 THEN ', DISABLE_DEF_CNST_CHECK' ELSE '' END + + CASE WHEN q.clientoption1 & 2 = 2 THEN ', IMPLICIT_TRANSACTIONS' ELSE '' END + + CASE WHEN q.clientoption1 & 4 = 4 THEN ', CURSOR_CLOSE_ON_COMMIT' ELSE '' END + + CASE WHEN q.clientoption1 & 8 = 8 THEN ', ANSI_WARNINGS' ELSE '' END + + CASE WHEN q.clientoption1 & 16 = 16 THEN ', ANSI_PADDING' ELSE '' END + + CASE WHEN q.clientoption1 & 32 = 32 THEN ', ANSI_NULLS' ELSE '' END + + CASE WHEN q.clientoption1 & 64 = 64 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 128 = 128 THEN ', ARITHIGNORE' ELSE '' END + + CASE WHEN q.clientoption1 & 256 = 256 THEN ', QUOTED_IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption1 & 512 = 512 THEN ', NOCOUNT' ELSE '' END + + CASE WHEN q.clientoption1 & 1024 = 1024 THEN ', ANSI_NULL_DFLT_ON' ELSE '' END + + CASE WHEN q.clientoption1 & 2048 = 2048 THEN ', ANSI_NULL_DFLT_OFF' ELSE '' END + + CASE WHEN q.clientoption1 & 4096 = 4096 THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + + CASE WHEN q.clientoption1 & 8192 = 8192 THEN ', NUMERIC_ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 16384 = 16384 THEN ', XACT_ABORT' ELSE '' END, + 3, + 500 + ), + client_option_2 = + SUBSTRING + ( + CASE WHEN q.clientoption2 & 1024 = 1024 THEN ', DB CHAINING' ELSE '' END + + CASE WHEN q.clientoption2 & 2048 = 2048 THEN ', NUMERIC ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 4096 = 4096 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 8192 = 8192 THEN ', ANSI PADDING' ELSE '' END + + CASE WHEN q.clientoption2 & 16384 = 16384 THEN ', ANSI NULL DEFAULT' ELSE '' END + + CASE WHEN q.clientoption2 & 65536 = 65536 THEN ', CONCAT NULL YIELDS NULL' ELSE '' END + + CASE WHEN q.clientoption2 & 131072 = 131072 THEN ', RECURSIVE TRIGGERS' ELSE '' END + + CASE WHEN q.clientoption2 & 1048576 = 1048576 THEN ', DEFAULT TO LOCAL CURSOR' ELSE '' END + + CASE WHEN q.clientoption2 & 8388608 = 8388608 THEN ', QUOTED IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption2 & 16777216 = 16777216 THEN ', AUTO CREATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 33554432 = 33554432 THEN ', CURSOR CLOSE ON COMMIT' ELSE '' END + + CASE WHEN q.clientoption2 & 67108864 = 67108864 THEN ', ANSI NULLS' ELSE '' END + + CASE WHEN q.clientoption2 & 268435456 = 268435456 THEN ', ANSI WARNINGS' ELSE '' END + + CASE WHEN q.clientoption2 & 536870912 = 536870912 THEN ', FULL TEXT ENABLED' ELSE '' END + + CASE WHEN q.clientoption2 & 1073741824 = 1073741824 THEN ', AUTO UPDATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 1469283328 = 1469283328 THEN ', ALL SETTABLE OPTIONS' ELSE '' END, + 3, + 500 + ), + q.process_xml + INTO #deadlock_process + FROM + ( + SELECT + dd.deadlock_xml, + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dd.event_date + ), + dd.victim_id, + is_parallel = + CONVERT(tinyint, dd.is_parallel) + + CONVERT(tinyint, dd.is_parallel_batch), + dd.deadlock_graph, + id = ca.dp.value('@id', 'nvarchar(256)'), + spid = ca.dp.value('@spid', 'smallint'), + database_id = ca.dp.value('@currentdb', 'bigint'), + current_database_name = ca.dp.value('@currentdbname', 'nvarchar(256)'), + priority = ca.dp.value('@priority', 'smallint'), + log_used = ca.dp.value('@logused', 'bigint'), + wait_resource = ca.dp.value('@waitresource', 'nvarchar(256)'), + wait_time = ca.dp.value('@waittime', 'bigint'), + transaction_name = ca.dp.value('@transactionname', 'nvarchar(256)'), + last_tran_started = ca.dp.value('@lasttranstarted', 'datetime'), + last_batch_started = ca.dp.value('@lastbatchstarted', 'datetime'), + last_batch_completed = ca.dp.value('@lastbatchcompleted', 'datetime'), + lock_mode = ca.dp.value('@lockMode', 'nvarchar(256)'), + status = ca.dp.value('@status', 'nvarchar(256)'), + transaction_count = ca.dp.value('@trancount', 'bigint'), + client_app = ca.dp.value('@clientapp', 'nvarchar(1024)'), + host_name = ca.dp.value('@hostname', 'nvarchar(256)'), + login_name = ca.dp.value('@loginname', 'nvarchar(256)'), + isolation_level = ca.dp.value('@isolationlevel', 'nvarchar(256)'), + clientoption1 = ca.dp.value('@clientoption1', 'bigint'), + clientoption2 = ca.dp.value('@clientoption2', 'bigint'), + process_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #dd AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) + WHERE (ca.dp.exist('@currentdb[. = sql:variable("@DatabaseId")]') = 1 OR @DatabaseName IS NULL) + AND (ca.dp.exist('@clientapp[. = sql:variable("@AppName")]') = 1 OR @AppName IS NULL) + AND (ca.dp.exist('@hostname[. = sql:variable("@HostName")]') = 1 OR @HostName IS NULL) + AND (ca.dp.exist('@loginname[. = sql:variable("@LoginName")]') = 1 OR @LoginName IS NULL) + ) AS q + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse execution stack xml*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse execution stack xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + dp.id, + dp.event_date, + proc_name = ca.dp.value('@procname', 'nvarchar(1024)'), + sql_handle = ca.dp.value('@sqlhandle', 'nvarchar(131)') + INTO #deadlock_stack + FROM #deadlock_process AS dp + CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) + WHERE (ca.dp.exist('@procname[. = sql:variable("@StoredProcName")]') = 1 OR @StoredProcName IS NULL) + AND ca.dp.exist('@sqlhandle[ .= "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"]') = 0 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Grab the full resource list*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + + RAISERROR('Grab the full resource list %s', 0, 1, @d) WITH NOWAIT; + + SELECT + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dr.event_date + ), + dr.victim_id, + dr.resource_xml + INTO + #deadlock_resource + FROM + ( + SELECT + event_date = dd.deadlock_xml.value('(event/@timestamp)[1]', 'datetime2'), + victim_id = dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'nvarchar(256)'), + resource_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #deadlock_data AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) + ) AS dr + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse object locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse object locks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + object_name = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + ca.object_name COLLATE Latin1_General_BIN2, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?'), + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = CAST(N'OBJECT' AS nvarchar(100)) + INTO #deadlock_owner_waiter + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse page locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse page locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'PAGE' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse key locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse key locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'KEY' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse RID locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse RID locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'RID' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse row group locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse row group locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'ROWGROUP' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/rowgrouplock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Fixing heaps in #deadlock_owner_waiter %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + d + SET + d.index_name = + d.object_name + N'.HEAP' + FROM #deadlock_owner_waiter AS d + WHERE d.lock_type IN + ( + N'HEAP', + N'RID' + ) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.id, + ca.event_date, + ca.wait_type, + ca.node_id, + ca.waiter_type, + ca.owner_activity, + ca.waiter_activity, + ca.merging, + ca.spilling, + ca.waiting_to_close, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)') + INTO #deadlock_resource_parallel + FROM + ( + SELECT + dr.event_date, + id = ca.dr.value('@id', 'nvarchar(256)'), + wait_type = ca.dr.value('@WaitType', 'nvarchar(256)'), + node_id = ca.dr.value('@nodeId', 'bigint'), + /* These columns are in 2017 CU5+ ONLY */ + waiter_type = ca.dr.value('@waiterType', 'nvarchar(256)'), + owner_activity = ca.dr.value('@ownerActivity', 'nvarchar(256)'), + waiter_activity = ca.dr.value('@waiterActivity', 'nvarchar(256)'), + merging = ca.dr.value('@merging', 'nvarchar(256)'), + spilling = ca.dr.value('@spilling', 'nvarchar(256)'), + waiting_to_close = ca.dr.value('@waitingToClose', 'nvarchar(256)'), + /* These columns are in 2017 CU5+ ONLY */ + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/exchangeEvent') AS ca(dr) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get rid of parallel noise*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of parallel noise %s', 0, 1, @d) WITH NOWAIT; + + WITH + c AS + ( + SELECT + *, + rn = + ROW_NUMBER() OVER + ( + PARTITION BY + drp.owner_id, + drp.waiter_id + ORDER BY + drp.event_date + ) + FROM #deadlock_resource_parallel AS drp + ) + DELETE + FROM c + WHERE c.rn > 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get rid of nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of nonsense %s', 0, 1, @d) WITH NOWAIT; + + DELETE dow + FROM #deadlock_owner_waiter AS dow + WHERE dow.owner_id = dow.waiter_id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Add some nonsense*/ + ALTER TABLE + #deadlock_process + ADD + waiter_mode nvarchar(256), + owner_mode nvarchar(256), + is_victim AS + CONVERT + ( + bit, + CASE + WHEN id = victim_id + THEN 1 + ELSE 0 + END + ) PERSISTED; + + /*Update some nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 1 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.owner_mode = dow.owner_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.id = dow.owner_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 0 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 2 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.waiter_mode = dow.waiter_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.victim_id = dow.waiter_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 1 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get Agent Job and Step names*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get Agent Job and Step names %s', 0, 1, @d) WITH NOWAIT; + + SELECT + x.event_date, + x.victim_id, + x.id, + x.database_id, + x.client_app, + x.job_id, + x.step_id, + job_id_guid = + CONVERT + ( + uniqueidentifier, + TRY_CAST + ( + N'' + AS xml + ).value('xs:hexBinary(substring(sql:column("x.job_id"), 0))', 'binary(16)') + ) + INTO #agent_job + FROM + ( + SELECT + dp.event_date, + dp.victim_id, + dp.id, + dp.database_id, + dp.client_app, + job_id = + SUBSTRING + ( + dp.client_app, + CHARINDEX(N'0x', dp.client_app) + LEN(N'0x'), + 32 + ), + step_id = + CASE + WHEN CHARINDEX(N': Step ', dp.client_app) > 0 + AND CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) > 0 + THEN + SUBSTRING + ( + dp.client_app, + CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step '), + CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) - + (CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step ')) + ) + ELSE dp.client_app + END + FROM #deadlock_process AS dp + WHERE dp.client_app LIKE N'SQLAgent - %' + AND dp.client_app <> N'SQLAgent - Initial Boot Probe' + ) AS x + OPTION(RECOMPILE); + + ALTER TABLE + #agent_job + ADD + job_name nvarchar(256), + step_name nvarchar(256); + + IF + ( + @Azure = 0 + AND @RDS = 0 + ) + BEGIN + SET @StringToExecute = + N' + UPDATE + aj + SET + aj.job_name = j.name, + aj.step_name = s.step_name + FROM msdb.dbo.sysjobs AS j + JOIN msdb.dbo.sysjobsteps AS s + ON j.job_id = s.job_id + JOIN #agent_job AS aj + ON aj.job_id_guid = j.job_id + AND aj.step_id = s.step_id + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + END; + + UPDATE + dp + SET + dp.client_app = + CASE + WHEN dp.client_app LIKE N'SQLAgent - %' + THEN N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name + ELSE dp.client_app + END + FROM #deadlock_process AS dp + JOIN #agent_job AS aj + ON dp.event_date = aj.event_date + AND dp.victim_id = aj.victim_id + AND dp.id = aj.id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get each and every table of all databases*/ + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId %s', 0, 1, @d) WITH NOWAIT; + + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + EXECUTE sys.sp_MSforeachdb + N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + USE [?]; + + IF EXISTS + ( + SELECT + 1/0 + FROM #deadlock_process AS dp + WHERE dp.database_id = DB_ID() + ) + BEGIN + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); + END; + '; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId at %s', 0, 1, @d) WITH NOWAIT; + + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*Begin checks based on parsed values*/ + + /* + First, revert these back since we already converted the event data to local time, + and searches will break if we use the times converted over to UTC for the event data + */ + SELECT + @StartDate = @StartDateOriginal, + @EndDate = @EndDateOriginal; + + /*Check 1 is deadlocks by database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 1 database deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 1, + dp.database_name, + object_name = N'-', + finding_group = N'Total Database Deadlocks', + finding = + N'This database had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 2 is deadlocks with selects*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 2 select deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 2, + dow.database_name, + object_name = + CASE + WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name COLLATE DATABASE_DEFAULT = dow.database_name COLLATE DATABASE_DEFAULT + AND d.is_read_committed_snapshot_on = 1 + ) + THEN N'You already enabled RCSI, but...' + ELSE N'You Might Need RCSI' + END, + finding_group = N'Total Deadlocks Involving Selects', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s) between read queries and modification queries.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND dow.lock_mode IN + ( + N'S', + N'IS' + ) + OR dow.owner_mode IN + ( + N'S', + N'IS' + ) + OR dow.waiter_mode IN + ( + N'S', + N'IS' + ) + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 is deadlocks by object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 object deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + object_name = + ISNULL + ( + dow.object_name, + N'UNKNOWN' + ), + finding_group = N'Total Object Deadlocks', + finding = + N'This object was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name, + dow.object_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 continuation, number of deadlocks per index*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) index deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Index Deadlocks', + finding = + N'This index was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type NOT IN + ( + N'HEAP', + N'RID' + ) + AND dow.index_name IS NOT NULL + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 continuation, number of deadlocks per heap*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) heap deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Heap Deadlocks', + finding = + N'This heap was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type IN + ( + N'HEAP', + N'RID' + ) + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 4 looks for Serializable deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 4 serializable deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 4, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Serializable Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Serializable deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'serializable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 5 looks for Repeatable Read deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 5 repeatable read deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 5, + dp.database_name, + object_name = N'-', + finding_group = N'Repeatable Read Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Repeatable Read deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'repeatable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 6 breaks down app, host, and login information*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 6 app/host/login deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 6, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Login, App, and Host deadlocks', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of deadlocks involving the login ' + + ISNULL + ( + dp.login_name, + N'UNKNOWN' + ) + + N' from the application ' + + ISNULL + ( + dp.client_app, + N'UNKNOWN' + ) + + N' on host ' + + ISNULL + ( + dp.host_name, + N'UNKNOWN' + ) + + N'.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY + dp.database_name, + dp.login_name, + dp.client_app, + dp.host_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 7 breaks down the types of deadlocks (object, page, key, etc.)*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 7 types of deadlocks %s', 0, 1, @d) WITH NOWAIT; + + WITH + lock_types AS + ( + SELECT + 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 + ) + ELSE dp.wait_resource + END, + 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 + GROUP BY + dp.database_name, + CASE + WHEN CHARINDEX(N':', dp.wait_resource) > 0 + THEN SUBSTRING + ( + dp.wait_resource, + 1, + CHARINDEX(N':', dp.wait_resource) - 1 + ) + ELSE dp.wait_resource + END, + dow.object_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 7, + lt.database_name, + lt.object_name, + finding_group = N'Types of locks by object', + finding = + N'This object has had ' + + STUFF + ( + ( + SELECT + 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'' + ) + N' locks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY CONVERT(bigint, lt.lock_count) DESC) + FROM lock_types AS lt + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 8 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 1 BlitzCache %s', 0, 1, @d) WITH NOWAIT; + + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.event_date, + ds.proc_name, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1), + sql_handle_csv = + N'''' + + STUFF + ( + ( + SELECT DISTINCT + N',' + + ds2.sql_handle + FROM #deadlock_stack AS ds2 + WHERE ds2.id = ds.id + AND ds2.event_date = ds.event_date + AND ds2.sql_handle <> 0x + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(MAX)'), + 1, + 1, + N'' + ) + + N'''' + FROM #deadlock_stack AS ds + WHERE ds.sql_handle <> 0x + GROUP BY + PARSENAME(ds.proc_name, 3), + PARSENAME(ds.proc_name, 2), + PARSENAME(ds.proc_name, 1), + ds.proc_name, + ds.id, + ds.event_date + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = N'EXECUTE sp_BlitzCache ' + + CASE + WHEN ds.proc_name = N'adhoc' + THEN N'@OnlySqlHandles = ' + ds.sql_handle_csv + ELSE N'@StoredProcName = ' + QUOTENAME(ds.proc_only_name, N'''') + END + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + AND ds.proc_name NOT LIKE 'Unknown%' + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + IF (@ProductVersionMajor >= 13 OR @Azure = 1) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 2 BlitzQueryStore %s', 0, 1, @d) WITH NOWAIT; + + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.sql_handle, + ds.proc_name, + ds.event_date, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1) + FROM #deadlock_stack AS ds + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = + N'EXECUTE sp_BlitzQueryStore ' + + N'@DatabaseName = ' + + QUOTENAME(ds.database_name, N'''') + + N', ' + + N'@StoredProcName = ' + + QUOTENAME(ds.proc_only_name, N'''') + + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE ds.proc_name <> N'adhoc' + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*Check 9 gives you stored procedure deadlock counts*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 9 stored procedure deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 9, + database_name = + dp.database_name, + object_name = ds.proc_name, + finding_group = N'Stored Procedure Deadlocks', + finding = + N'The stored procedure ' + + PARSENAME(ds.proc_name, 2) + + N'.' + + PARSENAME(ds.proc_name, 1) + + N' has been involved in ' + + CONVERT + ( + nvarchar(10), + COUNT_BIG(DISTINCT ds.id) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT ds.id) DESC) + FROM #deadlock_stack AS ds + JOIN #deadlock_process AS dp + ON dp.id = ds.id + AND ds.event_date = dp.event_date + WHERE ds.proc_name <> N'adhoc' + AND (ds.proc_name = @StoredProcName OR @StoredProcName IS NULL) + 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) + GROUP BY + dp.database_name, + ds.proc_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 10 gives you more info queries for sp_BlitzIndex */ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 10 more info, BlitzIndex %s', 0, 1, @d) WITH NOWAIT; + + WITH + bi AS + ( + SELECT DISTINCT + dow.object_name, + dow.database_name, + schema_name = s.schema_name, + table_name = s.table_name + FROM #deadlock_owner_waiter AS dow + JOIN @sysAssObjId AS s + ON s.database_id = dow.database_id + AND s.partition_id = dow.associatedObjectId + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 10, + bi.database_name, + bi.object_name, + finding_group = N'More Info - Table', + finding = + N'EXECUTE sp_BlitzIndex ' + + N'@DatabaseName = ' + + QUOTENAME(bi.database_name, N'''') + + N', @SchemaName = ' + + QUOTENAME(bi.schema_name, N'''') + + N', @TableName = ' + + QUOTENAME(bi.table_name, N'''') + + N';' + FROM bi + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 11 gets total deadlock wait time per object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 11 deadlock wait time per object %s', 0, 1, @d) WITH NOWAIT; + + WITH + chopsuey AS + ( + + SELECT + database_name = + dp.database_name, + dow.object_name, + wait_days = + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) / 1000 / 86400 + ) + ), + wait_time_hms = + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + ), + 0 + ), + 14 + ) + END, + total_waits = + SUM(CONVERT(bigint, dp.wait_time)) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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 (dow.object_name = @ObjectName OR @ObjectName 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) + GROUP BY + dp.database_name, + dow.object_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 11, + cs.database_name, + cs.object_name, + finding_group = N'Total object deadlock wait time', + finding = + N'This object has had ' + + CONVERT + ( + nvarchar(30), + cs.wait_days + ) + + N' ' + + CONVERT + ( + nvarchar(30), + cs.wait_time_hms, + 14 + ) + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY cs.total_waits DESC) + FROM chopsuey AS cs + WHERE cs.object_name IS NOT NULL + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 12 gets total deadlock wait time per database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 12 deadlock wait time per database %s', 0, 1, @d) WITH NOWAIT; + + WITH + wait_time AS + ( + SELECT + database_name = + dp.database_name, + total_wait_time_ms = + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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) + GROUP BY + dp.database_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 12, + wt.database_name, + object_name = N'-', + finding_group = N'Total database deadlock wait time', + N'This database has had ' + + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) / 1000 / 86400 + ) + ) + + N' ' + + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + ), + 0 + ), + 14 + ) END + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY SUM(CONVERT(bigint, wt.total_wait_time_ms)) DESC) + FROM wait_time AS wt + GROUP BY + wt.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 13 gets total deadlock wait time for SQL Agent*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 13 deadlock count for SQL Agent %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 13, + database_name = + DB_NAME(aj.database_id), + object_name = + N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name, + finding_group = N'Agent Job Deadlocks', + finding = + N'There have been ' + + RTRIM(COUNT_BIG(DISTINCT aj.event_date)) + + N' deadlocks from this Agent Job and Step.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT aj.event_date) DESC) + FROM #agent_job AS aj + GROUP BY + DB_NAME(aj.database_id), + aj.job_name, + aj.step_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 14 is total parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 14 parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total parallel deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT drp.event_date) + ) + + N' parallel deadlocks.' + FROM #deadlock_resource_parallel AS drp + HAVING COUNT_BIG(DISTINCT drp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 15 is total deadlocks involving sleeping sessions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 15 sleeping and background deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving sleeping sessions', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' sleepy deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.status = N'sleeping' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving background processes', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks with background task.' + FROM #deadlock_process AS dp + WHERE dp.status = N'background' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 16 is total deadlocks involving implicit transactions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 16 implicit transaction deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total implicit transaction deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' implicit transaction deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.transaction_name = N'implicit_transaction' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Thank you goodnight*/ + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + VALUES + ( + -1, + N'sp_BlitzLock version ' + CONVERT(nvarchar(10), @Version), + N'Results for ' + CONVERT(nvarchar(10), @StartDate, 23) + N' through ' + CONVERT(nvarchar(10), @EndDate, 23), + N'http://FirstResponderKit.org/', + N'To get help or add your own contributions to the SQL Server First Responder Kit, join us at http://FirstResponderKit.org.' + ); + + RAISERROR('Finished rollup at %s', 0, 1, @d) WITH NOWAIT; + + /*Results*/ + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results 1 %s', 0, 1, @d) WITH NOWAIT; + + CREATE CLUSTERED INDEX cx_whatever_dp ON #deadlock_process (event_date, id); + CREATE CLUSTERED INDEX cx_whatever_drp ON #deadlock_resource_parallel (event_date, owner_id); + CREATE CLUSTERED INDEX cx_whatever_dow ON #deadlock_owner_waiter (event_date, owner_id, waiter_id); + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + WITH + deadlocks AS + ( + SELECT + deadlock_type = + N'Regular Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + wait_resource = + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + dp.is_victim, + owner_mode = + ISNULL(dp.owner_mode, N'-'), + owner_waiter_type = NULL, + owner_activity = NULL, + owner_waiter_activity = NULL, + owner_merging = NULL, + owner_spilling = NULL, + owner_waiting_to_close = NULL, + waiter_mode = + ISNULL(dp.waiter_mode, N'-'), + waiter_waiter_type = NULL, + waiter_owner_activity = NULL, + waiter_waiter_activity = NULL, + waiter_merging = NULL, + waiter_spilling = NULL, + waiter_waiting_to_close = NULL, + dp.deadlock_graph + FROM #deadlock_process AS dp + WHERE dp.victim_id IS NOT NULL + AND dp.is_parallel = 0 + + UNION ALL + + SELECT + deadlock_type = + N'Parallel Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + is_victim = 1, + owner_mode = cao.wait_type COLLATE DATABASE_DEFAULT, + owner_waiter_type = cao.waiter_type, + owner_activity = cao.owner_activity, + owner_waiter_activity = cao.waiter_activity, + owner_merging = cao.merging, + owner_spilling = cao.spilling, + owner_waiting_to_close = cao.waiting_to_close, + waiter_mode = caw.wait_type COLLATE DATABASE_DEFAULT, + waiter_waiter_type = caw.waiter_type, + waiter_owner_activity = caw.owner_activity, + waiter_waiter_activity = caw.waiter_activity, + waiter_merging = caw.merging, + waiter_spilling = caw.spilling, + waiter_waiting_to_close = caw.waiting_to_close, + dp.deadlock_graph + FROM #deadlock_process AS dp + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeNewRow' + ) + ORDER BY drp.event_date + ) AS cao + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeGetRow' + ) + ORDER BY drp.event_date + ) AS caw + WHERE dp.is_parallel = 1 + ) + SELECT + d.deadlock_type, + d.event_date, + d.id, + d.victim_id, + d.spid, + deadlock_group = + N'Deadlock #' + + CONVERT + ( + nvarchar(10), + d.en + ) + + N', Query #' + + CASE + WHEN d.qn = 0 + THEN N'1' + ELSE CONVERT(nvarchar(10), d.qn) + END + CASE + WHEN d.is_victim = 1 + THEN N' - VICTIM' + ELSE N'' + END, + d.database_id, + d.database_name, + d.current_database_name, + d.priority, + d.log_used, + d.wait_resource, + d.object_names, + d.wait_time, + d.transaction_name, + d.status, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.lock_mode, + d.transaction_count, + d.client_app, + d.host_name, + d.login_name, + d.isolation_level, + d.client_option_1, + d.client_option_2, + inputbuf = + CASE + WHEN d.inputbuf + LIKE @inputbuf_bom + N'Proc |[Database Id = %' ESCAPE N'|' + THEN + OBJECT_SCHEMA_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + + N'.' + + OBJECT_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + ELSE d.inputbuf + END COLLATE Latin1_General_BIN2, + d.owner_mode, + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_mode, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + d.deadlock_graph, + d.is_victim + INTO #deadlocks + FROM deadlocks AS d + WHERE d.dn = 1 + AND (d.is_victim = @VictimsOnly + OR @VictimsOnly = 0) + AND d.qn < CASE + WHEN d.deadlock_type = N'Parallel Deadlock' + THEN 2 + ELSE 2147483647 + END + AND (DB_NAME(d.database_id) = @DatabaseName OR @DatabaseName IS NULL) + AND (d.event_date >= @StartDate OR @StartDate IS NULL) + AND (d.event_date < @EndDate OR @EndDate IS NULL) + AND (CONVERT(nvarchar(MAX), d.object_names) COLLATE Latin1_General_BIN2 LIKE N'%' + @ObjectName + N'%' OR @ObjectName IS NULL) + AND (d.client_app = @AppName OR @AppName IS NULL) + AND (d.host_name = @HostName OR @HostName IS NULL) + AND (d.login_name = @LoginName OR @LoginName IS NULL) + AND (d.deadlock_type = @DeadlockType OR @DeadlockType IS NULL) + OPTION (RECOMPILE, LOOP JOIN, HASH JOIN); + + UPDATE d + SET d.inputbuf = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + d.inputbuf, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?') + FROM #deadlocks AS d + OPTION(RECOMPILE); + + SELECT + d.deadlock_type, + d.event_date, + database_name = + DB_NAME(d.database_id), + database_name_x = + d.database_name, + d.current_database_name, + d.spid, + d.deadlock_group, + d.client_option_1, + d.client_option_2, + d.lock_mode, + query_xml = + ( + SELECT + [processing-instruction(query)] = + d.inputbuf + FOR XML + PATH(N''), + TYPE + ), + query_string = + d.inputbuf, + d.object_names, + d.isolation_level, + d.owner_mode, + d.waiter_mode, + d.transaction_count, + d.login_name, + d.host_name, + d.client_app, + d.wait_time, + d.wait_resource, + d.priority, + d.log_used, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.transaction_name, + d.status, + /*These columns will be NULL for regular (non-parallel) deadlocks*/ + parallel_deadlock_details = + ( + SELECT + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close + FOR XML + PATH('parallel_deadlock_details'), + TYPE + ), + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + /*end parallel deadlock columns*/ + d.deadlock_graph, + d.is_victim, + d.id + INTO #deadlock_results + FROM #deadlocks AS d; + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*There's too much risk of errors sending the*/ + IF @OutputDatabaseCheck = 0 + BEGIN + SET @ExportToExcel = 0; + END; + + SET @deadlock_result += N' + SELECT + server_name = + @@SERVERNAME, + dr.deadlock_type, + dr.event_date, + database_name = + COALESCE + ( + dr.database_name, + dr.database_name_x, + dr.current_database_name + ), + dr.spid, + dr.deadlock_group, + ' + CASE @ExportToExcel + WHEN 1 + THEN N' + query = dr.query_string, + object_names = + REPLACE( + REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.object_names + ) COLLATE Latin1_General_BIN2, + '''', ''''), + '''', ''''),' + ELSE N'query = dr.query_xml, + dr.object_names,' + END + N' + dr.isolation_level, + dr.owner_mode, + dr.waiter_mode, + dr.lock_mode, + dr.transaction_count, + dr.client_option_1, + dr.client_option_2, + dr.login_name, + dr.host_name, + dr.client_app, + dr.wait_time, + dr.wait_resource, + dr.priority, + dr.log_used, + dr.last_tran_started, + dr.last_batch_started, + dr.last_batch_completed, + dr.transaction_name, + dr.status,' + + CASE + WHEN (@ExportToExcel = 1 + OR @OutputDatabaseCheck = 0) + THEN N' + dr.owner_waiter_type, + dr.owner_activity, + dr.owner_waiter_activity, + dr.owner_merging, + dr.owner_spilling, + dr.owner_waiting_to_close, + dr.waiter_waiter_type, + dr.waiter_owner_activity, + dr.waiter_waiter_activity, + dr.waiter_merging, + dr.waiter_spilling, + dr.waiter_waiting_to_close,' + ELSE N' + dr.parallel_deadlock_details,' + END + + CASE + @ExportToExcel + WHEN 1 + THEN N' + deadlock_graph = + REPLACE(REPLACE( + REPLACE(REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.deadlock_graph + ) COLLATE Latin1_General_BIN2, + ''NCHAR(10)'', ''''), ''NCHAR(13)'', ''''), + ''CHAR(10)'', ''''), ''CHAR(13)'', '''')' + ELSE N' + dr.deadlock_graph' + END + N' + FROM #deadlock_results AS dr + ORDER BY + dr.event_date, + dr.is_victim DESC + OPTION(RECOMPILE, LOOP JOIN, HASH JOIN); + '; + + IF (@OutputDatabaseCheck = 0) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results to table %s', 0, 1, @d) WITH NOWAIT; + + IF @Debug = 1 + BEGIN + PRINT @deadlock_result; + SET STATISTICS XML ON; + END; + + SET @StringToExecute = N' + + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl + ( + ServerName, + deadlock_type, + event_date, + database_name, + spid, + deadlock_group, + query, + object_names, + isolation_level, + owner_mode, + waiter_mode, + lock_mode, + transaction_count, + client_option_1, + client_option_2, + login_name, + host_name, + client_app, + wait_time, + wait_resource, + priority, + log_used, + last_tran_started, + last_batch_started, + last_batch_completed, + transaction_name, + status, + owner_waiter_type, + owner_activity, + owner_waiter_activity, + owner_merging, + owner_spilling, + owner_waiting_to_close, + waiter_waiter_type, + waiter_owner_activity, + waiter_waiter_activity, + waiter_merging, + waiter_spilling, + waiter_waiting_to_close, + deadlock_graph + ) + EXECUTE sys.sp_executesql + @deadlock_result;'; + EXECUTE sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; + + IF @Debug = 1 + BEGIN + SET STATISTICS XML OFF; + END; + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + DROP SYNONYM dbo.DeadLockTbl; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; + + SET @StringToExecute = N' + + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings + ( + ServerName, + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + @@SERVERNAME, + df.check_id, + df.database_name, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY df.check_id + OPTION(RECOMPILE);'; + EXECUTE sys.sp_executesql @StringToExecute; + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ + END; + ELSE /*Output to database is not set output to client app*/ + 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; + + 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', + ds.proc_name, + sql_handle = + CONVERT(varbinary(64), ds.sql_handle, 1), + dow.database_name, + dow.database_id, + dow.object_name, + query_xml = + TRY_CAST(dr.query_xml AS nvarchar(MAX)) + INTO #available_plans + FROM #deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + JOIN #deadlock_results AS dr + ON dr.id = ds.id + AND dr.event_date = ds.event_date + OPTION(RECOMPILE); + + SELECT + deqs.sql_handle, + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset, + deqs.creation_time, + deqs.last_execution_time, + deqs.execution_count, + total_worker_time_ms = + deqs.total_worker_time / 1000., + avg_worker_time_ms = + CONVERT(decimal(38, 6), deqs.total_worker_time / 1000. / deqs.execution_count), + total_elapsed_time_ms = + deqs.total_elapsed_time / 1000., + avg_elapsed_time_ms = + CONVERT(decimal(38, 6), deqs.total_elapsed_time / 1000. / deqs.execution_count), + executions_per_second = + ISNULL + ( + deqs.execution_count / + NULLIF + ( + DATEDIFF + ( + SECOND, + deqs.creation_time, + NULLIF(deqs.last_execution_time, '1900-01-01 00:00:00.000') + ), + 0 + ), + 0 + ), + total_physical_reads_mb = + deqs.total_physical_reads * 8. / 1024., + total_logical_writes_mb = + deqs.total_logical_writes * 8. / 1024., + total_logical_reads_mb = + deqs.total_logical_reads * 8. / 1024., + min_grant_mb = + deqs.min_grant_kb * 8. / 1024., + max_grant_mb = + deqs.max_grant_kb * 8. / 1024., + min_used_grant_mb = + deqs.min_used_grant_kb * 8. / 1024., + max_used_grant_mb = + 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 = + deqs.max_worker_time / 1000., + max_elapsed_time_ms = + deqs.max_elapsed_time / 1000. + INTO #dm_exec_query_stats + FROM sys.dm_exec_query_stats AS deqs + WHERE EXISTS + ( + SELECT + 1/0 + FROM #available_plans AS ap + WHERE ap.sql_handle = deqs.sql_handle + ) + AND deqs.query_hash IS NOT NULL; + + CREATE CLUSTERED INDEX + deqs + ON #dm_exec_query_stats + ( + sql_handle, + plan_handle + ); + + SELECT + ap.available_plans, + ap.database_name, + query_text = + TRY_CAST(ap.query_xml AS xml), + ap.query_plan, + ap.creation_time, + ap.last_execution_time, + ap.execution_count, + ap.executions_per_second, + ap.total_worker_time_ms, + ap.avg_worker_time_ms, + ap.max_worker_time_ms, + ap.total_elapsed_time_ms, + ap.avg_elapsed_time_ms, + ap.max_elapsed_time_ms, + ap.total_logical_reads_mb, + ap.total_physical_reads_mb, + ap.total_logical_writes_mb, + ap.min_grant_mb, + ap.max_grant_mb, + ap.min_used_grant_mb, + ap.max_used_grant_mb, + ap.min_reserved_threads, + ap.max_reserved_threads, + ap.min_used_threads, + ap.max_used_threads, + ap.total_rows, + ap.sql_handle, + ap.statement_start_offset, + ap.statement_end_offset + FROM + ( + + SELECT + ap.*, + c.statement_start_offset, + c.statement_end_offset, + c.creation_time, + c.last_execution_time, + c.execution_count, + c.total_worker_time_ms, + c.avg_worker_time_ms, + c.total_elapsed_time_ms, + c.avg_elapsed_time_ms, + c.executions_per_second, + c.total_physical_reads_mb, + c.total_logical_writes_mb, + c.total_logical_reads_mb, + c.min_grant_mb, + c.max_grant_mb, + c.min_used_grant_mb, + c.max_used_grant_mb, + c.min_reserved_threads, + c.max_reserved_threads, + c.min_used_threads, + c.max_used_threads, + c.total_rows, + c.query_plan, + c.max_worker_time_ms, + c.max_elapsed_time_ms + FROM #available_plans AS ap + OUTER APPLY + ( + SELECT + deqs.*, + query_plan = + TRY_CAST(deps.query_plan AS xml) + FROM #dm_exec_query_stats deqs + OUTER APPLY sys.dm_exec_text_query_plan + ( + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset + ) AS deps + WHERE deqs.sql_handle = ap.sql_handle + AND deps.dbid = ap.database_id + ) AS c + ) AS ap + WHERE ap.query_plan IS NOT NULL + ORDER BY + ap.avg_worker_time_ms DESC + 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, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY + 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.*/ + END; + + IF @Debug = 1 + BEGIN + SELECT + table_name = N'#deadlock_data', + * + FROM #deadlock_data AS dd + OPTION(RECOMPILE); + + SELECT + table_name = N'#dd', + * + FROM #dd AS d + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_resource', + * + FROM #deadlock_resource AS dr + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_resource_parallel', + * + FROM #deadlock_resource_parallel AS drp + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_owner_waiter', + * + FROM #deadlock_owner_waiter AS dow + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_process', + * + FROM #deadlock_process AS dp + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_stack', + * + FROM #deadlock_stack AS ds + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlocks', + * + FROM #deadlocks AS d + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_results', + * + FROM #deadlock_results AS dr + OPTION(RECOMPILE); + + SELECT + table_name = N'#x', + * + FROM #x AS x + OPTION(RECOMPILE); + + SELECT + table_name = N'@sysAssObjId', + * + FROM @sysAssObjId AS s + OPTION(RECOMPILE); + + IF OBJECT_ID('tempdb..#available_plans') IS NOT NULL + BEGIN + SELECT + table_name = N'#available_plans', + * + FROM #available_plans AS ap + OPTION(RECOMPILE); + END; + + IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL + BEGIN + SELECT + table_name = N'#dm_exec_query_stats', + * + FROM #dm_exec_query_stats + OPTION(RECOMPILE); + END; + + SELECT + procedure_parameters = + 'procedure_parameters', + DatabaseName = + @DatabaseName, + StartDate = + @StartDate, + EndDate = + @EndDate, + ObjectName = + @ObjectName, + StoredProcName = + @StoredProcName, + AppName = + @AppName, + HostName = + @HostName, + LoginName = + @LoginName, + EventSessionName = + @EventSessionName, + TargetSessionType = + @TargetSessionType, + VictimsOnly = + @VictimsOnly, + DeadlockType = + @DeadlockType, + TargetDatabaseName = + @TargetDatabaseName, + TargetSchemaName = + @TargetSchemaName, + TargetTableName = + @TargetTableName, + TargetColumnName = + @TargetColumnName, + TargetTimestampColumnName = + @TargetTimestampColumnName, + Debug = + @Debug, + Help = + @Help, + Version = + @Version, + VersionDate = + @VersionDate, + VersionCheckMode = + @VersionCheckMode, + OutputDatabaseName = + @OutputDatabaseName, + OutputSchemaName = + @OutputSchemaName, + OutputTableName = + @OutputTableName, + ExportToExcel = + @ExportToExcel; + + SELECT + declared_variables = + 'declared_variables', + DatabaseId = + @DatabaseId, + StartDateUTC = + @StartDateUTC, + EndDateUTC = + @EndDateUTC, + ProductVersion = + @ProductVersion, + ProductVersionMajor = + @ProductVersionMajor, + ProductVersionMinor = + @ProductVersionMinor, + ObjectFullName = + @ObjectFullName, + Azure = + @Azure, + RDS = + @RDS, + d = + @d, + StringToExecute = + @StringToExecute, + StringToExecuteParams = + @StringToExecuteParams, + r = + @r, + OutputTableFindings = + @OutputTableFindings, + DeadlockCount = + @DeadlockCount, + ServerName = + @ServerName, + OutputDatabaseCheck = + @OutputDatabaseCheck, + SessionId = + @SessionId, + TargetSessionId = + @TargetSessionId, + FileName = + @FileName, + inputbuf_bom = + @inputbuf_bom, + deadlock_result = + @deadlock_result; + END; /*End debug*/ + END; /*Final End*/ +GO +IF OBJECT_ID('dbo.sp_BlitzWho') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_BlitzWho AS RETURN 0;') +GO + +ALTER PROCEDURE dbo.sp_BlitzWho + @Help TINYINT = 0 , + @ShowSleepingSPIDs TINYINT = 0, + @ExpertMode BIT = 0, + @Debug BIT = 0, + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputTableRetentionDays TINYINT = 3 , + @MinElapsedSeconds INT = 0 , + @MinCPUTime INT = 0 , + @MinLogicalReads INT = 0 , + @MinPhysicalReads INT = 0 , + @MinWrites INT = 0 , + @MinTempdbMB INT = 0 , + @MinRequestedMemoryKB INT = 0 , + @MinBlockingSeconds INT = 0 , + @CheckDateOverride DATETIMEOFFSET = NULL, + @ShowActualParameters BIT = 0, + @GetOuterCommand BIT = 0, + @GetLiveQueryPlan BIT = NULL, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @SortOrder NVARCHAR(256) = N'elapsed time' +AS +BEGIN + SET NOCOUNT ON; + SET STATISTICS XML OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; + + + + IF @Help = 1 + BEGIN + PRINT ' +sp_BlitzWho from http://FirstResponderKit.org + +This script gives you a snapshot of everything currently executing on your SQL Server. + +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. + +Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Outputting to table is only supported with SQL Server 2012 and higher. + - If @OutputDatabaseName and @OutputSchemaName are populated, the database and + schema must already exist. We will not create them, only the table. + +MIT License + +Copyright (c) Brent Ozar Unlimited + +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. +'; +RETURN; +END; /* @Help = 1 */ + +/* Get the major and minor build numbers */ +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]) + ,@EnhanceFlag BIT = 0 + ,@BlockingCheck NVARCHAR(MAX) + ,@StringToSelect NVARCHAR(MAX) + ,@StringToExecute NVARCHAR(MAX) + ,@OutputTableCleanupDate DATE + ,@SessionWaits BIT = 0 + ,@SessionWaitsSQL NVARCHAR(MAX) = + N'LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT TOP 5 waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_time_ms) AS NVARCHAR(128)) + + N'' ms), '' + FROM sys.dm_exec_session_wait_stats AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + HAVING SUM(waitwait.wait_time_ms) > 5 + ORDER BY 1 + FOR + XML PATH('''') ) AS session_wait_info + FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 + ON s.session_id = wt2.session_id + LEFT JOIN sys.dm_exec_query_stats AS session_stats + ON r.sql_handle = session_stats.sql_handle + AND r.plan_handle = session_stats.plan_handle + AND r.statement_start_offset = session_stats.statement_start_offset + AND r.statement_end_offset = session_stats.statement_end_offset' + ,@ObjectFullName NVARCHAR(2000) + ,@OutputTableNameQueryStats_View NVARCHAR(256) + ,@LineFeed NVARCHAR(MAX) /* Had to set as MAX up from 10 as it was truncating the view creation*/; + +/* Let's get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = REPLACE(LOWER(@SortOrder), N' ', N'_'); + +SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), + @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) + +SELECT + @OutputTableNameQueryStats_View = QUOTENAME(@OutputTableName + '_Deltas'), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @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 + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ExpertMode = 1; /* Force ExpertMode when we're logging to table */ + + /* Create the table if it doesn't exist */ + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'('; + SET @StringToExecute = @StringToExecute + N' + ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128) NOT NULL, + CheckDate DATETIMEOFFSET NOT NULL, + [elapsed_time] [varchar](41) NULL, + [session_id] [smallint] NOT NULL, + [database_name] [nvarchar](128) NULL, + [query_text] [nvarchar](max) NULL, + [outer_command] NVARCHAR(4000) NULL, + [query_plan] [xml] NULL, + [live_query_plan] [xml] NULL, + [cached_parameter_info] [nvarchar](max) NULL, + [live_parameter_info] [nvarchar](max) NULL, + [query_cost] [float] NULL, + [status] [nvarchar](30) NOT NULL, + [wait_info] [nvarchar](max) NULL, + [wait_resource] [nvarchar](max) NULL, + [top_session_waits] [nvarchar](max) NULL, + [blocking_session_id] [smallint] NULL, + [open_transaction_count] [int] NULL, + [is_implicit_transaction] [int] NOT NULL, + [nt_domain] [nvarchar](128) NULL, + [host_name] [nvarchar](128) NULL, + [login_name] [nvarchar](128) NOT NULL, + [nt_user_name] [nvarchar](128) NULL, + [program_name] [nvarchar](128) NULL, + [fix_parameter_sniffing] [nvarchar](150) NULL, + [client_interface_name] [nvarchar](32) NULL, + [login_time] [datetime] NOT NULL, + [start_time] [datetime] NULL, + [request_time] [datetime] NULL, + [request_cpu_time] [int] NULL, + [request_logical_reads] [bigint] NULL, + [request_writes] [bigint] NULL, + [request_physical_reads] [bigint] NULL, + [session_cpu] [int] NOT NULL, + [session_logical_reads] [bigint] NOT NULL, + [session_physical_reads] [bigint] NOT NULL, + [session_writes] [bigint] NOT NULL, + [tempdb_allocations_mb] [decimal](38, 2) NULL, + [memory_usage] [int] NOT NULL, + [estimated_completion_time] [bigint] NULL, + [percent_complete] [real] NULL, + [deadlock_priority] [int] NULL, + [transaction_isolation_level] [varchar](33) NOT NULL, + [degree_of_parallelism] [smallint] NULL, + [last_dop] [bigint] NULL, + [min_dop] [bigint] NULL, + [max_dop] [bigint] NULL, + [last_grant_kb] [bigint] NULL, + [min_grant_kb] [bigint] NULL, + [max_grant_kb] [bigint] NULL, + [last_used_grant_kb] [bigint] NULL, + [min_used_grant_kb] [bigint] NULL, + [max_used_grant_kb] [bigint] NULL, + [last_ideal_grant_kb] [bigint] NULL, + [min_ideal_grant_kb] [bigint] NULL, + [max_ideal_grant_kb] [bigint] NULL, + [last_reserved_threads] [bigint] NULL, + [min_reserved_threads] [bigint] NULL, + [max_reserved_threads] [bigint] NULL, + [last_used_threads] [bigint] NULL, + [min_used_threads] [bigint] NULL, + [max_used_threads] [bigint] NULL, + [grant_time] [varchar](20) NULL, + [requested_memory_kb] [bigint] NULL, + [grant_memory_kb] [bigint] NULL, + [is_request_granted] [varchar](39) NOT NULL, + [required_memory_kb] [bigint] NULL, + [query_memory_grant_used_memory_kb] [bigint] NULL, + [ideal_memory_kb] [bigint] NULL, + [is_small] [bit] NULL, + [timeout_sec] [int] NULL, + [resource_semaphore_id] [smallint] NULL, + [wait_order] [varchar](20) NULL, + [wait_time_ms] [varchar](20) NULL, + [next_candidate_for_memory_grant] [varchar](3) NOT NULL, + [target_memory_kb] [bigint] NULL, + [max_target_memory_kb] [varchar](30) NULL, + [total_memory_kb] [bigint] NULL, + [available_memory_kb] [bigint] NULL, + [granted_memory_kb] [bigint] NULL, + [query_resource_semaphore_used_memory_kb] [bigint] NULL, + [grantee_count] [int] NULL, + [waiter_count] [int] NULL, + [timeout_error_count] [bigint] NULL, + [forced_grant_count] [varchar](30) NULL, + [workload_group_name] [sysname] NULL, + [resource_pool_name] [sysname] NULL, + [context_info] [varchar](128) NULL, + [query_hash] [binary](8) NULL, + [query_plan_hash] [binary](8) NULL, + [sql_handle] [varbinary] (64) NULL, + [plan_handle] [varbinary] (64) NULL, + [statement_start_offset] INT NULL, + [statement_end_offset] INT NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC(@StringToExecute); + + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new cached_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''cached_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD cached_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new live_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''live_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD live_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new outer_command column, add it. See Github #2887. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''outer_command'') + ALTER TABLE ' + @ObjectFullName + N' ADD outer_command NVARCHAR(4000) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new wait_resource column, add it. See Github #2970. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''wait_resource'') + ALTER TABLE ' + @ObjectFullName + N' ADD wait_resource NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* Delete history older than @OutputTableRetentionDays */ + SET @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + N''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + N' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @@SERVERNAME, @OutputTableCleanupDate; + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameQueryStats_View; + + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameQueryStats_View + N' AS ' + @LineFeed + + N'WITH MaxQueryDuration AS ' + @LineFeed + + N'( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' MIN([ID]) AS [MinID], ' + @LineFeed + + N' MAX([ID]) AS [MaxID] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' GROUP BY [ServerName], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [sql_handle] ' + @LineFeed + + N') ' + @LineFeed + + N'SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @LineFeed + + N' ( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' /* Truncate the query text to aid performance of painting the rows in SSMS */ ' + @LineFeed + + N' CAST([query_text] AS NVARCHAR(1000)) AS [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' ((CAST([request_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' ((CAST([request_writes] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' ((CAST([request_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' ((CAST([session_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' ((CAST([session_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' ((CAST([session_writes] AS DECIMAL(38,2))* 8)/ 1024) [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' ) AS [BlitzWho] ' + @LineFeed + + N'INNER JOIN [MaxQueryDuration] ON [BlitzWho].[ID] = [MaxQueryDuration].[MaxID]; ' + @LineFeed + + N''');' + + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + + EXEC(@StringToExecute); + END; + + END + + IF OBJECT_ID('tempdb..#WhoReadableDBs') IS NOT NULL + DROP TABLE #WhoReadableDBs; + +CREATE TABLE #WhoReadableDBs +( +database_id INT +); + +IF 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; + + EXEC('INSERT INTO #WhoReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'); +END + +SELECT @BlockingCheck = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SET LOCK_TIMEOUT 1000; /* To avoid blocking on live query plans. See Github issue #2907. */ + DECLARE @blocked TABLE + ( + dbid SMALLINT NOT NULL, + last_batch DATETIME NOT NULL, + open_tran SMALLINT NOT NULL, + sql_handle BINARY(20) NOT NULL, + session_id SMALLINT NOT NULL, + blocking_session_id SMALLINT NOT NULL, + lastwaittype NCHAR(32) NOT NULL, + waittime BIGINT NOT NULL, + cpu INT NOT NULL, + physical_io BIGINT NOT NULL, + memusage INT NOT NULL + ); + + INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) + SELECT + sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, + sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage + FROM sys.sysprocesses AS sys1 + JOIN sys.sysprocesses AS sys2 + ON sys1.spid = sys2.blocked; + '+CASE + WHEN (@GetOuterCommand = 1 AND (NOT EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer'))) THEN N' + DECLARE @session_id SMALLINT; + DECLARE @Sessions TABLE + ( + session_id INT + ); + + DECLARE @inputbuffer TABLE + ( + ID INT IDENTITY(1,1), + session_id INT, + event_type NVARCHAR(30), + parameters SMALLINT, + event_info NVARCHAR(4000) + ); + + DECLARE inputbuffer_cursor + + CURSOR LOCAL FAST_FORWARD + FOR + SELECT session_id + FROM sys.dm_exec_sessions + WHERE session_id <> @@SPID + AND is_user_process = 1; + + OPEN inputbuffer_cursor; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id; + + WHILE (@@FETCH_STATUS = 0) + BEGIN; + BEGIN TRY; + + INSERT INTO @inputbuffer ([event_type],[parameters],[event_info]) + EXEC sp_executesql + N''DBCC INPUTBUFFER(@session_id) WITH NO_INFOMSGS;'', + N''@session_id SMALLINT'', + @session_id; + + UPDATE @inputbuffer + SET session_id = @session_id + WHERE ID = SCOPE_IDENTITY(); + + END TRY + BEGIN CATCH + RAISERROR(''DBCC inputbuffer failed for session %d'',0,0,@session_id) WITH NOWAIT; + END CATCH; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id + + END; + + CLOSE inputbuffer_cursor; + DEALLOCATE inputbuffer_cursor;' + ELSE N'' + END+ + N' + + DECLARE @LiveQueryPlans TABLE + ( + Session_Id INT NOT NULL, + Query_Plan XML NOT NULL + ); + + ' +IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan' AND @GetLiveQueryPlan=1) +BEGIN + SET @BlockingCheck = @BlockingCheck + N' + INSERT INTO @LiveQueryPlans + SELECT s.session_id, query_plan + FROM sys.dm_exec_sessions AS s + CROSS APPLY sys.dm_exec_query_statistics_xml(s.session_id) + WHERE s.session_id <> @@SPID;'; +END + + +IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 +BEGIN + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SET @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id + ELSE NULL + END AS blocking_session_id, + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource , + COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END + + IF @ExpertMode = 1 + BEGIN + SET @StringToExecute += + N', + ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , + COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, + tempdb_allocations.tempdb_allocations_mb, + s.memory_usage , + r.estimated_completion_time , + r.percent_complete , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , + qmg.dop AS degree_of_parallelism , + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, + wg.name AS workload_group_name , + rp.name AS resource_pool_name, + CONVERT(VARCHAR(128), r.context_info) AS context_info + ' + END /* IF @ExpertMode = 1 */ + + SET @StringToExecute += + N'FROM sys.dm_exec_sessions AS s + '+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N'OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N'LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle + AND r.plan_handle = query_stats.plan_handle + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id + AND r.request_id = qmg.request_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.resource_governor_workload_groups wg + ON s.group_id = wg.group_id + LEFT JOIN sys.resource_governor_resource_pools rp + ON wg.pool_id = rp.pool_id + OUTER APPLY ( + SELECT TOP 1 + b.dbid, b.last_batch, b.open_tran, b.sql_handle, + b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime + FROM @blocked b + WHERE (s.session_id = b.session_id + OR s.session_id = b.blocking_session_id) + ) AS blocked + OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY ( + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb + FROM sys.dm_db_task_space_usage tsu + WHERE tsu.request_id = r.request_id + AND tsu.session_id = r.session_id + AND tsu.session_id = s.session_id + ) as tempdb_allocations + WHERE s.session_id <> @@SPID + AND s.host_name IS NOT NULL + ' + + CASE WHEN @ShowSleepingSPIDs = 0 THEN + N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' + WHEN @ShowSleepingSPIDs = 1 THEN + N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' + ELSE N'' END; +END /* IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 */ + +IF @ProductVersionMajor >= 11 + BEGIN + SELECT @EnhanceFlag = + CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 + WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 + WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 + WHEN @ProductVersionMajor > 13 THEN 1 + ELSE 0 + END + + + IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL + BEGIN + SET @SessionWaits = 1 + END + + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SELECT @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id + ELSE NULL + END AS blocking_session_id, + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 + THEN '''''' + ELSE '''''' + END + +') AS XML + + + ) AS live_query_plan , + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + FROM derp.query_plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Cached_Parameter_Info, + ' + IF @ShowActualParameters = 1 + BEGIN + SELECT @StringToExecute = @StringToExecute + N'qs_live.Live_Parameter_Info as Live_Parameter_Info,' + END + + SELECT @StringToExecute = @StringToExecute + N' + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource ,' + + + CASE @SessionWaits + WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' + ELSE N' NULL AS top_session_waits ,' + END + + + N'COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END + + IF @ExpertMode = 1 /* We show more columns in expert mode, so the SELECT gets longer */ + BEGIN + SET @StringToExecute += + N', ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , + COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, + tempdb_allocations.tempdb_allocations_mb, + s.memory_usage , + r.estimated_completion_time , + r.percent_complete , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , + qmg.dop AS degree_of_parallelism , ' + + + CASE @EnhanceFlag + WHEN 1 THEN N'query_stats.last_dop, + query_stats.min_dop, + query_stats.max_dop, + query_stats.last_grant_kb, + query_stats.min_grant_kb, + query_stats.max_grant_kb, + query_stats.last_used_grant_kb, + query_stats.min_used_grant_kb, + query_stats.max_used_grant_kb, + query_stats.last_ideal_grant_kb, + query_stats.min_ideal_grant_kb, + query_stats.max_ideal_grant_kb, + query_stats.last_reserved_threads, + query_stats.min_reserved_threads, + query_stats.max_reserved_threads, + query_stats.last_used_threads, + query_stats.min_used_threads, + query_stats.max_used_threads,' + ELSE N' NULL AS last_dop, + NULL AS min_dop, + NULL AS max_dop, + NULL AS last_grant_kb, + NULL AS min_grant_kb, + NULL AS max_grant_kb, + NULL AS last_used_grant_kb, + NULL AS min_used_grant_kb, + NULL AS max_used_grant_kb, + NULL AS last_ideal_grant_kb, + NULL AS min_ideal_grant_kb, + NULL AS max_ideal_grant_kb, + NULL AS last_reserved_threads, + NULL AS min_reserved_threads, + NULL AS max_reserved_threads, + NULL AS last_used_threads, + NULL AS min_used_threads, + NULL AS max_used_threads,' + END + + SET @StringToExecute += + N' + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, + wg.name AS workload_group_name, + rp.name AS resource_pool_name, + CONVERT(VARCHAR(128), r.context_info) AS context_info, + r.query_hash, r.query_plan_hash, r.sql_handle, r.plan_handle, r.statement_start_offset, r.statement_end_offset ' + END /* IF @ExpertMode = 1 */ + + SET @StringToExecute += + N' FROM sys.dm_exec_sessions AS s'+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N' + OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N' + LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle + AND r.plan_handle = query_stats.plan_handle + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + ' + + + CASE @SessionWaits + WHEN 1 THEN @SessionWaitsSQL + ELSE N'' + END + + + N' + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id + AND r.request_id = qmg.request_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.resource_governor_workload_groups wg + ON s.group_id = wg.group_id + LEFT JOIN sys.resource_governor_resource_pools rp + ON wg.pool_id = rp.pool_id + OUTER APPLY ( + SELECT TOP 1 + b.dbid, b.last_batch, b.open_tran, b.sql_handle, + b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime + FROM @blocked b + WHERE (s.session_id = b.session_id + OR s.session_id = b.blocking_session_id) + ) AS blocked + OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY ( + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb + FROM sys.dm_db_task_space_usage tsu + WHERE tsu.request_id = r.request_id + AND tsu.session_id = r.session_id + AND tsu.session_id = s.session_id + ) as tempdb_allocations + + OUTER APPLY ( + SELECT TOP 1 Query_Plan, + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + N'' (Actual: '' + Node.Data.value(''(@ParameterRuntimeValue)[1]'', ''NVARCHAR(4000)'') + N'')'' + FROM q.Query_Plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Live_Parameter_Info + FROM @LiveQueryPlans q + WHERE (s.session_id = q.Session_Id) + + ) AS qs_live + + WHERE s.session_id <> @@SPID + AND s.host_name IS NOT NULL + AND r.database_id NOT IN (SELECT database_id FROM #WhoReadableDBs) + ' + + CASE WHEN @ShowSleepingSPIDs = 0 THEN + N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' + WHEN @ShowSleepingSPIDs = 1 THEN + N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' + ELSE N'' END; + + +END /* IF @ProductVersionMajor >= 11 */ + +IF (@MinElapsedSeconds + @MinCPUTime + @MinLogicalReads + @MinPhysicalReads + @MinWrites + @MinTempdbMB + @MinRequestedMemoryKB + @MinBlockingSeconds) > 0 + BEGIN + /* They're filtering for something, so set up a where clause that will let any (not all combined) of the min triggers work: */ + SET @StringToExecute += N' AND (1 = 0 '; + IF @MinElapsedSeconds > 0 + SET @StringToExecute += N' OR ABS(COALESCE(r.total_elapsed_time,0)) / 1000 >= ' + CAST(@MinElapsedSeconds AS NVARCHAR(20)); + IF @MinCPUTime > 0 + SET @StringToExecute += N' OR COALESCE(r.cpu_time, s.cpu_time,0) / 1000 >= ' + CAST(@MinCPUTime AS NVARCHAR(20)); + IF @MinLogicalReads > 0 + SET @StringToExecute += N' OR COALESCE(r.logical_reads, s.logical_reads,0) >= ' + CAST(@MinLogicalReads AS NVARCHAR(20)); + IF @MinPhysicalReads > 0 + SET @StringToExecute += N' OR COALESCE(s.reads,0) >= ' + CAST(@MinPhysicalReads AS NVARCHAR(20)); + IF @MinWrites > 0 + SET @StringToExecute += N' OR COALESCE(r.writes, s.writes,0) >= ' + CAST(@MinWrites AS NVARCHAR(20)); + IF @MinTempdbMB > 0 + SET @StringToExecute += N' OR COALESCE(tempdb_allocations.tempdb_allocations_mb,0) >= ' + CAST(@MinTempdbMB AS NVARCHAR(20)); + IF @MinRequestedMemoryKB > 0 + SET @StringToExecute += N' OR COALESCE(qmg.requested_memory_kb,0) >= ' + CAST(@MinRequestedMemoryKB AS NVARCHAR(20)); + /* Blocking is a little different - we're going to return ALL of the queries if we meet the blocking threshold. */ + IF @MinBlockingSeconds > 0 + SET @StringToExecute += N' OR (SELECT SUM(waittime / 1000) FROM @blocked) >= ' + CAST(@MinBlockingSeconds AS NVARCHAR(20)); + SET @StringToExecute += N' ) '; + END + +SET @StringToExecute += + N' ORDER BY ' + CASE WHEN @SortOrder = 'session_id' THEN '[session_id] DESC' + WHEN @SortOrder = 'query_cost' THEN '[query_cost] DESC' + WHEN @SortOrder = 'database_name' THEN '[database_name] ASC' + WHEN @SortOrder = 'open_transaction_count' THEN '[open_transaction_count] DESC' + WHEN @SortOrder = 'is_implicit_transaction' THEN '[is_implicit_transaction] DESC' + WHEN @SortOrder = 'login_name' THEN '[login_name] ASC' + WHEN @SortOrder = 'program_name' THEN '[program_name] ASC' + WHEN @SortOrder = 'client_interface_name' THEN '[client_interface_name] ASC' + WHEN @SortOrder = 'request_cpu_time' THEN 'COALESCE(r.cpu_time, s.cpu_time) DESC' + WHEN @SortOrder = 'request_logical_reads' THEN 'COALESCE(r.logical_reads, s.logical_reads) DESC' + WHEN @SortOrder = 'request_writes' THEN 'COALESCE(r.writes, s.writes) DESC' + WHEN @SortOrder = 'request_physical_reads' THEN 'COALESCE(r.reads, s.reads) DESC ' + WHEN @SortOrder = 'session_cpu' THEN 's.cpu_time DESC' + WHEN @SortOrder = 'session_logical_reads' THEN 's.logical_reads DESC' + WHEN @SortOrder = 'session_physical_reads' THEN 's.reads DESC' + WHEN @SortOrder = 'session_writes' THEN 's.writes DESC' + WHEN @SortOrder = 'tempdb_allocations_mb' THEN '[tempdb_allocations_mb] DESC' + WHEN @SortOrder = 'memory_usage' THEN '[memory_usage] DESC' + WHEN @SortOrder = 'deadlock_priority' THEN 'r.deadlock_priority DESC' + WHEN @SortOrder = 'transaction_isolation_level' THEN 'r.[transaction_isolation_level] DESC' + WHEN @SortOrder = 'requested_memory_kb' THEN '[requested_memory_kb] DESC' + WHEN @SortOrder = 'grant_memory_kb' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'grant' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'query_memory_grant_used_memory_kb' THEN 'qmg.used_memory_kb DESC' + WHEN @SortOrder = 'ideal_memory_kb' THEN '[ideal_memory_kb] DESC' + WHEN @SortOrder = 'workload_group_name' THEN 'wg.name ASC' + WHEN @SortOrder = 'resource_pool_name' THEN 'rp.name ASC' + ELSE '[elapsed_time] DESC' + END + ' + '; + + +IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + N'; ' + + @BlockingCheck + + + ' INSERT INTO ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'(ServerName + ,CheckDate + ,[elapsed_time] + ,[session_id] + ,[blocking_session_id] + ,[database_name] + ,[query_text]' + + CASE WHEN @GetOuterCommand = 1 THEN N',[outer_command]' ELSE N'' END + N' + ,[query_plan]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[live_query_plan]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[cached_parameter_info]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 AND @ShowActualParameters = 1 THEN N',[Live_Parameter_Info]' ELSE N'' END + N' + ,[query_cost] + ,[status] + ,[wait_info] + ,[wait_resource]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[top_session_waits]' ELSE N'' END + N' + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[request_logical_reads] + ,[request_writes] + ,[request_physical_reads] + ,[session_cpu] + ,[session_logical_reads] + ,[session_physical_reads] + ,[session_writes] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[degree_of_parallelism]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads]' ELSE N'' END + N' + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset]' ELSE N'' END + N' +) + SELECT @@SERVERNAME, COALESCE(@CheckDateOverride, SYSDATETIMEOFFSET()) AS CheckDate , ' + + @StringToExecute; + END +ELSE + SET @StringToExecute = @BlockingCheck + N' SELECT GETDATE() AS run_date , ' + @StringToExecute; + +/* If the server has > 50GB of memory, add a max grant hint to avoid getting a giant grant */ +IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020) + OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 ) + OR (@ProductVersionMajor >= 13 ) + AND 50000000 < (SELECT cntr_value + FROM sys.dm_os_performance_counters + WHERE object_name LIKE '%:Memory Manager%' + AND counter_name LIKE 'Target Server Memory (KB)%') + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (MAX_GRANT_PERCENT = 1, RECOMPILE) '; + END +ELSE + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (RECOMPILE) '; + END + +/* Be good: */ +SET @StringToExecute = @StringToExecute + N' ; '; + + +IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + +EXEC sp_executesql @StringToExecute, + N'@CheckDateOverride DATETIMEOFFSET', + @CheckDateOverride; + +END +GO diff --git a/Install-Core-Blitz-No-Query-Store.sql b/Install-Core-Blitz-No-Query-Store.sql deleted file mode 100644 index a2c862173..000000000 --- a/Install-Core-Blitz-No-Query-Store.sql +++ /dev/null @@ -1,23946 +0,0 @@ -IF OBJECT_ID('dbo.sp_Blitz') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0;'); -GO - -ALTER PROCEDURE [dbo].[sp_Blitz] - @Help TINYINT = 0 , - @CheckUserDatabaseObjects TINYINT = 1 , - @CheckProcedureCache TINYINT = 0 , - @OutputType VARCHAR(20) = 'TABLE' , - @OutputProcedureCache TINYINT = 0 , - @CheckProcedureCacheFilter VARCHAR(10) = NULL , - @CheckServerInfo TINYINT = 0 , - @SkipChecksServer NVARCHAR(256) = NULL , - @SkipChecksDatabase NVARCHAR(256) = NULL , - @SkipChecksSchema NVARCHAR(256) = NULL , - @SkipChecksTable NVARCHAR(256) = NULL , - @IgnorePrioritiesBelow INT = NULL , - @IgnorePrioritiesAbove INT = NULL , - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @OutputXMLasNVARCHAR TINYINT = 0 , - @EmailRecipients VARCHAR(MAX) = NULL , - @EmailProfile sysname = NULL , - @SummaryMode TINYINT = 0 , - @BringThePain TINYINT = 0 , - @Debug TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; - SET @OutputType = UPPER(@OutputType); - - IF @Help = 1 PRINT ' - /* - sp_Blitz from http://FirstResponderKit.org - - This script checks the health of your SQL Server and gives you a prioritized - to-do list of the most urgent things you should consider fixing. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - If a database name has a question mark in it, some tests will fail. Gotta - love that unsupported sp_MSforeachdb. - - If you have offline databases, sp_Blitz fails the first time you run it, - but does work the second time. (Hoo, boy, this will be fun to debug.) - - @OutputServerName will output QueryPlans as NVARCHAR(MAX) since Microsoft - has refused to support XML columns in Linked Server queries. The bug is now - 16 years old! *~ \o/ ~* - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. - @CheckServerInfo 1=show server info like CPUs, memory, virtualization - @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. - @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm - @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' - @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list | ''SCHEMA''=version and field list | ''NONE'' = none - @IgnorePrioritiesBelow 50=ignore priorities below 50 - @IgnorePrioritiesAbove 50=ignore priorities above 50 - For the rest of the parameters, see https://www.BrentOzar.com/blitz/documentation for details. - - - - MIT License - - Copyright for portions of sp_Blitz are held by Microsoft as part of project - tigertoolbox and are provided under the MIT license: - https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - - - - */'; - ELSE IF @OutputType = 'SCHEMA' - BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT'; - - END; - ELSE /* IF @OutputType = 'SCHEMA' */ - BEGIN - - DECLARE @StringToExecute NVARCHAR(4000) - ,@curr_tracefilename NVARCHAR(500) - ,@base_tracefilename NVARCHAR(500) - ,@indx int - ,@query_result_separator CHAR(1) - ,@EmailSubject NVARCHAR(255) - ,@EmailBody NVARCHAR(MAX) - ,@EmailAttachmentFilename NVARCHAR(255) - ,@ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@CurrentName NVARCHAR(128) - ,@CurrentDefaultValue NVARCHAR(200) - ,@CurrentCheckID INT - ,@CurrentPriority INT - ,@CurrentFinding VARCHAR(200) - ,@CurrentURL VARCHAR(200) - ,@CurrentDetails NVARCHAR(4000) - ,@MsSinceWaitsCleared DECIMAL(38,0) - ,@CpuMsSinceWaitsCleared DECIMAL(38,0) - ,@ResultText NVARCHAR(MAX) - ,@crlf NVARCHAR(2) - ,@Processors int - ,@NUMANodes int - ,@MinServerMemory bigint - ,@MaxServerMemory bigint - ,@ColumnStoreIndexesInUse bit - ,@TraceFileIssue bit - -- Flag for Windows OS to help with Linux support - ,@IsWindowsOperatingSystem BIT; - - - SET @crlf = NCHAR(13) + NCHAR(10); - SET @ResultText = 'sp_Blitz Results: ' + @crlf; - - /* - --TOURSTOP01-- - See https://www.BrentOzar.com/go/blitztour for a guided tour. - - We start by creating #BlitzResults. It's a temp table that will store all of - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. - - #BlitzResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - For a list of checks, visit http://FirstResponderKit.org. - */ - IF OBJECT_ID('tempdb..#BlitzResults') IS NOT NULL - DROP TABLE #BlitzResults; - CREATE TABLE #BlitzResults - ( - ID INT IDENTITY(1, 1) , - CheckID INT , - DatabaseName NVARCHAR(128) , - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL - ); - - IF OBJECT_ID('tempdb..#TemporaryDatabaseResults') IS NOT NULL - DROP TABLE #TemporaryDatabaseResults; - CREATE TABLE #TemporaryDatabaseResults - ( - DatabaseName NVARCHAR(128) , - Finding NVARCHAR(128) - ); - - /* - You can build your own table with a list of checks to skip. For example, you - might have some databases that you don't care about, or some checks you don't - want to run. Then, when you run sp_Blitz, you can specify these parameters: - @SkipChecksDatabase = 'DBAtools', - @SkipChecksSchema = 'dbo', - @SkipChecksTable = 'BlitzChecksToSkip' - Pass in the database, schema, and table that contains the list of checks you - want to skip. This part of the code checks those parameters, gets the list, - and then saves those in a temp table. As we run each check, we'll see if we - need to skip it. - - Really anal-retentive users will note that the @SkipChecksServer parameter is - not used. YET. We added that parameter in so that we could avoid changing the - stored proc's surface area (interface) later. - */ - /* --TOURSTOP07-- */ - IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL - DROP TABLE #SkipChecks; - CREATE TABLE #SkipChecks - ( - DatabaseName NVARCHAR(128) , - CheckID INT , - ServerName NVARCHAR(128) - ); - CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); - - IF @SkipChecksTable IS NOT NULL - AND @SkipChecksSchema IS NOT NULL - AND @SkipChecksDatabase IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Inserting SkipChecks', 0, 1) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) - SELECT DISTINCT DatabaseName, CheckID, ServerName - FROM ' + QUOTENAME(@SkipChecksDatabase) + '.' + QUOTENAME(@SkipChecksSchema) + '.' + QUOTENAME(@SkipChecksTable) - + ' WHERE ServerName IS NULL OR ServerName = SERVERPROPERTY(''ServerName'') OPTION (RECOMPILE);'; - EXEC(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - BEGIN - -- Flag for Windows OS to help with Linux support - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN - SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; - END; - ELSE - BEGIN - SELECT @IsWindowsOperatingSystem = 1 ; - END; - - select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; - set @curr_tracefilename = reverse(@curr_tracefilename); - - -- Set the trace file path separator based on underlying OS - IF (@IsWindowsOperatingSystem = 1) - BEGIN - select @indx = patindex('%\%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '\log.trc' ; - END; - ELSE - BEGIN - select @indx = patindex('%/%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '/log.trc' ; - END; - - END; - - /* If the server has any databases on Antiques Roadshow, skip the checks that would break due to CTEs. */ - IF @CheckUserDatabaseObjects = 1 AND EXISTS(SELECT * FROM sys.databases WHERE compatibility_level < 90) - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Databases with compatibility level < 90 found, so setting @CheckUserDatabaseObjects = 0.'; - PRINT 'The database-level checks rely on CTEs, which are not supported in SQL 2000 compat level databases.'; - PRINT 'Get with the cool kids and switch to a current compatibility level, Grandpa. To find the problems, run:'; - PRINT 'SELECT * FROM sys.databases WHERE compatibility_level < 90;'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 204 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'Since you have databases with compatibility_level < 90, we can''t run @CheckUserDatabaseObjects = 1. To find them: SELECT * FROM sys.databases WHERE compatibility_level < 90' AS Details; - END; - - - /* --TOURSTOP08-- */ - /* If the server is Amazon RDS, skip checks that it doesn't allow */ - IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('ServerName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (6); - INSERT INTO #SkipChecks (CheckID) VALUES (29); - INSERT INTO #SkipChecks (CheckID) VALUES (30); - INSERT INTO #SkipChecks (CheckID) VALUES (31); - INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file */ - INSERT INTO #SkipChecks (CheckID) VALUES (57); - INSERT INTO #SkipChecks (CheckID) VALUES (59); - INSERT INTO #SkipChecks (CheckID) VALUES (61); - INSERT INTO #SkipChecks (CheckID) VALUES (62); - INSERT INTO #SkipChecks (CheckID) VALUES (68); - INSERT INTO #SkipChecks (CheckID) VALUES (69); - INSERT INTO #SkipChecks (CheckID) VALUES (73); - INSERT INTO #SkipChecks (CheckID) VALUES (79); - INSERT INTO #SkipChecks (CheckID) VALUES (92); - INSERT INTO #SkipChecks (CheckID) VALUES (94); - INSERT INTO #SkipChecks (CheckID) VALUES (96); - INSERT INTO #SkipChecks (CheckID) VALUES (98); - INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled */ - INSERT INTO #SkipChecks (CheckID) VALUES (123); - INSERT INTO #SkipChecks (CheckID) VALUES (177); - INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans */ - INSERT INTO #SkipChecks (CheckID) VALUES (181); - END; /* Amazon RDS skipped checks */ - - /* If the server is ExpressEdition, skip checks that it doesn't allow */ - IF CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (30); /* Alerts not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (31); /* Operators not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (61); /* Agent alerts 19-25 */ - INSERT INTO #SkipChecks (CheckID) VALUES (73); /* Failsafe operator */ - INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ - END; /* Express Edition skipped checks */ - - - /* - That's the end of the SkipChecks stuff. - The next several tables are used by various checks later. - */ - IF OBJECT_ID('tempdb..#ConfigurationDefaults') IS NOT NULL - DROP TABLE #ConfigurationDefaults; - CREATE TABLE #ConfigurationDefaults - ( - name NVARCHAR(128) , - DefaultValue BIGINT, - CheckID INT - ); - - IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL - DROP TABLE #Recompile; - CREATE TABLE #Recompile( - DBName varchar(200), - ProcName varchar(300), - RecompileFlag varchar(1), - SPSchema varchar(50) - ); - - IF OBJECT_ID('tempdb..#DatabaseDefaults') IS NOT NULL - DROP TABLE #DatabaseDefaults; - CREATE TABLE #DatabaseDefaults - ( - name NVARCHAR(128) , - DefaultValue NVARCHAR(200), - CheckID INT, - Priority INT, - Finding VARCHAR(200), - URL VARCHAR(200), - Details NVARCHAR(4000) - ); - - IF OBJECT_ID('tempdb..#DatabaseScopedConfigurationDefaults') IS NOT NULL - DROP TABLE #DatabaseScopedConfigurationDefaults; - CREATE TABLE #DatabaseScopedConfigurationDefaults - (ID INT IDENTITY(1,1), configuration_id INT, [name] NVARCHAR(60), default_value sql_variant, default_value_for_secondary sql_variant, CheckID INT, ); - - - - IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL - DROP TABLE #DBCCs; - CREATE TABLE #DBCCs - ( - ID INT IDENTITY(1, 1) - PRIMARY KEY , - ParentObject VARCHAR(255) , - Object VARCHAR(255) , - Field VARCHAR(255) , - Value VARCHAR(255) , - DbName NVARCHAR(128) NULL - ); - - - IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL - DROP TABLE #LogInfo2012; - CREATE TABLE #LogInfo2012 - ( - recoveryunitid INT , - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); - - IF OBJECT_ID('tempdb..#LogInfo') IS NOT NULL - DROP TABLE #LogInfo; - CREATE TABLE #LogInfo - ( - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); - - IF OBJECT_ID('tempdb..#partdb') IS NOT NULL - DROP TABLE #partdb; - CREATE TABLE #partdb - ( - dbname NVARCHAR(128) , - objectname NVARCHAR(200) , - type_desc NVARCHAR(128) - ); - - IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); - - IF OBJECT_ID('tempdb..#driveInfo') IS NOT NULL - DROP TABLE #driveInfo; - CREATE TABLE #driveInfo - ( - drive NVARCHAR , - SIZE DECIMAL(18, 2) - ); - - - IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL - DROP TABLE #dm_exec_query_stats; - CREATE TABLE #dm_exec_query_stats - ( - [id] [int] NOT NULL - IDENTITY(1, 1) , - [sql_handle] [varbinary](64) NOT NULL , - [statement_start_offset] [int] NOT NULL , - [statement_end_offset] [int] NOT NULL , - [plan_generation_num] [bigint] NOT NULL , - [plan_handle] [varbinary](64) NOT NULL , - [creation_time] [datetime] NOT NULL , - [last_execution_time] [datetime] NOT NULL , - [execution_count] [bigint] NOT NULL , - [total_worker_time] [bigint] NOT NULL , - [last_worker_time] [bigint] NOT NULL , - [min_worker_time] [bigint] NOT NULL , - [max_worker_time] [bigint] NOT NULL , - [total_physical_reads] [bigint] NOT NULL , - [last_physical_reads] [bigint] NOT NULL , - [min_physical_reads] [bigint] NOT NULL , - [max_physical_reads] [bigint] NOT NULL , - [total_logical_writes] [bigint] NOT NULL , - [last_logical_writes] [bigint] NOT NULL , - [min_logical_writes] [bigint] NOT NULL , - [max_logical_writes] [bigint] NOT NULL , - [total_logical_reads] [bigint] NOT NULL , - [last_logical_reads] [bigint] NOT NULL , - [min_logical_reads] [bigint] NOT NULL , - [max_logical_reads] [bigint] NOT NULL , - [total_clr_time] [bigint] NOT NULL , - [last_clr_time] [bigint] NOT NULL , - [min_clr_time] [bigint] NOT NULL , - [max_clr_time] [bigint] NOT NULL , - [total_elapsed_time] [bigint] NOT NULL , - [last_elapsed_time] [bigint] NOT NULL , - [min_elapsed_time] [bigint] NOT NULL , - [max_elapsed_time] [bigint] NOT NULL , - [query_hash] [binary](8) NULL , - [query_plan_hash] [binary](8) NULL , - [query_plan] [xml] NULL , - [query_plan_filtered] [nvarchar](MAX) NULL , - [text] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL , - [text_filtered] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL - ); - - IF OBJECT_ID('tempdb..#ErrorLog') IS NOT NULL - DROP TABLE #ErrorLog; - CREATE TABLE #ErrorLog - ( - LogDate DATETIME , - ProcessInfo NVARCHAR(20) , - [Text] NVARCHAR(1000) - ); - - IF OBJECT_ID('tempdb..#fnTraceGettable') IS NOT NULL - DROP TABLE #fnTraceGettable; - CREATE TABLE #fnTraceGettable - ( - TextData NVARCHAR(4000) , - DatabaseName NVARCHAR(256) , - EventClass INT , - Severity INT , - StartTime DATETIME , - EndTime DATETIME , - Duration BIGINT , - NTUserName NVARCHAR(256) , - NTDomainName NVARCHAR(256) , - HostName NVARCHAR(256) , - ApplicationName NVARCHAR(256) , - LoginName NVARCHAR(256) , - DBUserName NVARCHAR(256) - ); - - IF OBJECT_ID('tempdb..#IgnorableWaits') IS NOT NULL - DROP TABLE #IgnorableWaits; - CREATE TABLE #IgnorableWaits (wait_type NVARCHAR(60)); - INSERT INTO #IgnorableWaits VALUES ('BROKER_EVENTHANDLER'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_RECEIVE_WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TASK_STOP'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TO_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TRANSMITTER'); - INSERT INTO #IgnorableWaits VALUES ('CHECKPOINT_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('CLR_AUTO_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_MANUAL_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_EVENTS_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_WORKER_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRRORING_CMD'); - INSERT INTO #IgnorableWaits VALUES ('DIRTY_PAGE_POLL'); - INSERT INTO #IgnorableWaits VALUES ('DISPATCHER_QUEUE_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTSHC_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('HADR_CLUSAPI_CALL'); - INSERT INTO #IgnorableWaits VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION'); - INSERT INTO #IgnorableWaits VALUES ('HADR_LOGCAPTURE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('HADR_NOTIFICATION_DEQUEUE'); - INSERT INTO #IgnorableWaits VALUES ('HADR_TIMER_TASK'); - INSERT INTO #IgnorableWaits VALUES ('HADR_WORK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('LAZYWRITER_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('LOGMGR_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('ONDEMAND_TASK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS'); - INSERT INTO #IgnorableWaits VALUES ('QDS_ASYNC_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_SHUTDOWN_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('REDO_THREAD_PENDING_WORK'); - INSERT INTO #IgnorableWaits VALUES ('REQUEST_FOR_DEADLOCK_SEARCH'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_SYSTEMTASK'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_TASK'); - INSERT INTO #IgnorableWaits VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_BUFFER_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('UCS_SESSION_REGISTRATION'); - INSERT INTO #IgnorableWaits VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'); - INSERT INTO #IgnorableWaits VALUES ('WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('XE_DISPATCHER_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('XE_LIVE_TARGET_TVF'); - INSERT INTO #IgnorableWaits VALUES ('XE_TIMER_EVENT'); - - IF @Debug IN (1, 2) RAISERROR('Setting @MsSinceWaitsCleared', 0, 1) WITH NOWAIT; - - SELECT @MsSinceWaitsCleared = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) * 60000.0 - FROM sys.databases - WHERE name = 'tempdb'; - - /* Have they cleared wait stats? Using a 10% fudge factor */ - IF @MsSinceWaitsCleared * .9 > (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 185) WITH NOWAIT; - - SET @MsSinceWaitsCleared = (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')); - IF @MsSinceWaitsCleared = 0 SET @MsSinceWaitsCleared = 1; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES( 185, - 240, - 'Wait Stats', - 'Wait Stats Have Been Cleared', - 'https://BrentOzar.com/go/waits', - 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + CONVERT(NVARCHAR(100), DATEADD(ms, (-1 * @MsSinceWaitsCleared), GETDATE()), 120)); - END; - - /* @CpuMsSinceWaitsCleared is used for waits stats calculations */ - - IF @Debug IN (1, 2) RAISERROR('Setting @CpuMsSinceWaitsCleared', 0, 1) WITH NOWAIT; - - SELECT @CpuMsSinceWaitsCleared = @MsSinceWaitsCleared * scheduler_count - FROM sys.dm_os_sys_info; - - - /* If we're outputting CSV or Markdown, don't bother checking the plan cache because we cannot export plans. */ - IF @OutputType = 'CSV' OR @OutputType = 'MARKDOWN' - SET @CheckProcedureCache = 0; - - /* If we're posting a question on Stack, include background info on the server */ - IF @OutputType = 'MARKDOWN' - SET @CheckServerInfo = 1; - - - /* Only run CheckUserDatabaseObjects if there are less than 50 databases. */ - IF @BringThePain = 0 AND 50 <= (SELECT COUNT(*) FROM sys.databases) AND @CheckUserDatabaseObjects = 1 - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary insanity for the server and/or user.'; - PRINT 'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 201 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'If you want to check 50+ databases, you have to also use @BringThePain = 1.' AS Details; - END; - - /* Sanitize our inputs */ - SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); - - /* Get the major and minor build numbers */ - - IF @Debug IN (1, 2) RAISERROR('Getting version information.', 0, 1) WITH NOWAIT; - - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(varchar(32), @ProductVersion), 2); - - /* - Whew! we're finally done with the setup, and we can start doing checks. - First, let's make sure we're actually supposed to do checks on this server. - The user could have passed in a SkipChecks table that specified to skip ALL - checks on this server, so let's check for that: - */ - IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID IS NULL ) ) - OR ( @SkipChecksTable IS NULL ) - ) - BEGIN - - /* - Our very first check! We'll put more comments in this one just to - explain exactly how it works. First, we check to see if we're - supposed to skip CheckID 1 (that's the check we're working on.) - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 1 ) - BEGIN - - /* - Below, we check master.sys.databases looking for databases - that haven't had a backup in the last week. If we find any, - we insert them into #BlitzResults, the temp table that - tracks our server's problems. Note that if the check does - NOT find any problems, we don't save that. We're only - saving the problems, not the successful checks. - */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 1) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 1 AS CheckID , - d.[name] AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backups Not Performed Recently' AS Finding , - 'https://BrentOzar.com/go/nobak' AS URL , - 'Last backed up: ' - + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details - FROM master.sys.databases d - LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'D' - AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ - WHERE d.database_id <> 2 /* Bonus points if you know what that means */ - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 1) - /* - The above NOT IN filters out the databases we're not supposed to check. - */ - GROUP BY d.name - HAVING MAX(b.backup_finish_date) <= DATEADD(dd, - -7, GETDATE()) - OR MAX(b.backup_finish_date) IS NULL; - /* - And there you have it. The rest of this stored procedure works the same - way: it asks: - - Should I skip this check? - - If not, do I find problems? - - Insert the results into #BlitzResults - */ - - END; - - /* - And that's the end of CheckID #1. - - CheckID #2 is a little simpler because it only involves one query, and it's - more typical for queries that people contribute. But keep reading, because - the next check gets more complex again. - */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 2 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 2 AS CheckID , - d.name AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Full Recovery Model w/o Log Backups' AS Finding , - 'https://BrentOzar.com/go/biglogs' AS URL , - ( 'The ' + CAST(CAST((SELECT ((SUM([mf].[size]) * 8.) / 1024.) FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = d.[database_id] AND [mf].[type_desc] = 'LOG') AS DECIMAL(18,2)) AS VARCHAR(30)) + 'MB log file has not been backed up in the last week.' ) AS Details - FROM master.sys.databases d - WHERE d.recovery_model IN ( 1, 2 ) - AND d.database_id NOT IN ( 2, 3 ) - AND d.source_database_id IS NULL - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 2) - AND NOT EXISTS ( SELECT * - FROM msdb.dbo.backupset b - WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'L' - AND b.backup_finish_date >= DATEADD(dd, - -7, GETDATE()) ); - END; - - - /* - Next up, we've got CheckID 8. (These don't have to go in order.) This one - won't work on SQL Server 2005 because it relies on a new DMV that didn't - exist prior to SQL Server 2008. This means we have to check the SQL Server - version first, then build a dynamic string with the query we want to run: - */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 8 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 8) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, Priority, - FindingsGroup, - Finding, URL, - Details) - SELECT 8 AS CheckID, - 230 AS Priority, - ''Security'' AS FindingsGroup, - ''Server Audits Running'' AS Finding, - ''https://BrentOzar.com/go/audits'' AS URL, - (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* - But what if you need to run a query in every individual database? - Hop down to the @CheckUserDatabaseObjects section. - - And that's the basic idea! You can read through the rest of the - checks if you like - some more exciting stuff happens closer to the - end of the stored proc, where we start doing things like checking - the plan cache, but those aren't as cleanly commented. - - If you'd like to contribute your own check, use one of the check - formats shown above and email it to Help@BrentOzar.com. You don't - have to pick a CheckID or a link - we'll take care of that when we - test and publish the code. Thanks! - */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 93 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 93) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 93 AS CheckID , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backing Up to Same Drive Where Databases Reside' AS Finding , - 'https://BrentOzar.com/go/backup' AS URL , - CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' - + UPPER(LEFT(bmf.physical_device_name, 3)) - + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details - FROM msdb.dbo.backupmediafamily AS bmf - INNER JOIN msdb.dbo.backupset AS bs ON bmf.media_set_id = bs.media_set_id - AND bs.backup_start_date >= ( DATEADD(dd, - -14, GETDATE()) ) - /* Filter out databases that were recently restored: */ - LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name AND rh.restore_date > DATEADD(dd, -14, GETDATE()) - WHERE UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( - SELECT DISTINCT - UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) - FROM sys.master_files AS mf ) - AND rh.destination_database_name IS NULL - GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 119 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_database_encryption_keys' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 119) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, DatabaseName, URL, Details) - SELECT 119 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''TDE Certificate Not Backed Up Recently'' AS Finding, - db_name(dek.database_id) AS DatabaseName, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 202 ) - AND EXISTS ( SELECT * - FROM sys.all_columns c - WHERE c.name = 'pvt_key_last_backup_date' ) - AND EXISTS ( SELECT * - FROM msdb.INFORMATION_SCHEMA.COLUMNS c - WHERE c.TABLE_NAME = 'backupset' AND c.COLUMN_NAME = 'encryptor_thumbprint' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 202 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''Encryption Certificate Not Backed Up Recently'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database backups. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c - INNER JOIN msdb.dbo.backupset bs ON c.thumbprint = bs.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 3 ) - BEGIN - IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 3) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 3 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Not Purged' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 186 ) - BEGIN - IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 186) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 186 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Purged Too Frequently' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history only retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 178 ) - AND EXISTS (SELECT * - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 178) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 178 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Snapshot Backups Occurring' AS Finding , - 'https://BrentOzar.com/go/snaps' AS URL , - ( CAST(COUNT(*) AS VARCHAR(20)) + ' snapshot-looking backups have occurred in the last two weeks, indicating that IO may be freezing up.') AS Details - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()); /* In the last 2 weeks */ - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 4 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 4) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 4 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Sysadmins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.sysadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0 - AND l.name NOT LIKE 'NT SERVICE\%' - AND l.name <> 'l_certSignSmDetach'; /* Added in SQL 2016 */ - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 5 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 5) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 5 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Security Admins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.securityadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 104 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 104) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] - ) - SELECT 104 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Login Can Control Server' AS [Finding] , - 'https://BrentOzar.com/go/sa' AS [URL] , - 'Login [' + pri.[name] - + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] - FROM sys.server_principals AS pri - WHERE pri.[principal_id] IN ( - SELECT p.[grantee_principal_id] - FROM sys.server_permissions AS p - WHERE p.[state] IN ( 'G', 'W' ) - AND p.[class] = 100 - AND p.[type] = 'CL' ) - AND pri.[name] NOT LIKE '##%##'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 6 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 6 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Jobs Owned By Users' AS Finding , - 'https://BrentOzar.com/go/owners' AS URL , - ( 'Job [' + j.name + '] is owned by [' - + SUSER_SNAME(j.owner_sid) - + '] - 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); - END; - - - /* --TOURSTOP06-- */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 7 ) - BEGIN - /* --TOURSTOP02-- */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 7) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 7 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Stored Procedure Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Stored procedure [master].[' - + r.SPECIFIC_SCHEMA + '].[' - + r.SPECIFIC_NAME - + '] runs automatically when SQL Server starts up. Make sure you know exactly what this stored procedure is doing, because it could pose a security risk.' ) AS Details - FROM master.INFORMATION_SCHEMA.ROUTINES r - WHERE OBJECTPROPERTY(OBJECT_ID(ROUTINE_NAME), - 'ExecIsStartup') = 1; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 10 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 10) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 10 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Resource Governor Enabled'' AS Finding, - ''https://BrentOzar.com/go/rg'' AS URL, - (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 11 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 11) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 11 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Server Triggers Enabled'' AS Finding, - ''https://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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 12 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 12) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 12 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Close Enabled' AS Finding , - 'https://BrentOzar.com/go/autoclose' AS URL , - ( 'Database [' + [name] - + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_close_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 12); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 13 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 13) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 13 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Enabled' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - ( 'Database [' + [name] - + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_shrink_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 13); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 14 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 14) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 14 AS CheckID, - [name] as DatabaseName, - 50 AS Priority, - ''Reliability'' AS FindingsGroup, - ''Page Verification Not Optimal'' AS Finding, - ''https://BrentOzar.com/go/torn'' AS URL, - (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details - FROM sys.databases - WHERE page_verify_option < 2 - AND name <> ''tempdb'' - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 14) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 15 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 15) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 15 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Create Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/acs' AS URL , - ( 'Database [' + [name] - + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_create_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 15); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 16 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 16) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 16 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Update Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/aus' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 16); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 17 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 17) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 17 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Stats Updated Asynchronously' AS Finding , - 'https://BrentOzar.com/go/asyncstats' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_async_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 17); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 18 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 18) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 18 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Forced Parameterization On' AS Finding , - 'https://BrentOzar.com/go/forced' AS URL , - ( 'Database [' + [name] - + '] has forced parameterization enabled. SQL Server will aggressively reuse query execution plans even if the applications do not parameterize their queries. This can be a performance booster with some programming languages, or it may use universally bad execution plans when better alternatives are available for certain parameters.' ) AS Details - FROM sys.databases - WHERE is_parameterization_forced = 1 - AND name NOT IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 18); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 20 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 20 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Date Correlation On' AS Finding , - 'https://BrentOzar.com/go/corr' AS URL , - ( 'Database [' + [name] - + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details - FROM sys.databases - WHERE is_date_correlation_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 20); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 21 ) - BEGIN - /* --TOURSTOP04-- */ - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 21) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 21 AS CheckID, - [name] as DatabaseName, - 200 AS Priority, - ''Informational'' AS FindingsGroup, - ''Database Encrypted'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details - FROM sys.databases - WHERE is_encrypted = 1 - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 21) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* - Believe it or not, SQL Server doesn't track the default values - for sp_configure options! We'll make our own list here. - */ - - 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; - 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 ); - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 22 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 22) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT cd.CheckID , - 200 AS Priority , - 'Non-Default Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/conf' AS URL , - ( 'This sp_configure option has been changed. Its default value is ' - + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), - '(unknown)') - + ' and it has been set to ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '.' ) AS Details - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - LEFT OUTER JOIN #ConfigurationDefaults cdUsed ON cdUsed.name = cr.name - AND cdUsed.DefaultValue = cr.value_in_use - WHERE cdUsed.name IS NULL; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 190 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Setting @MinServerMemory and @MaxServerMemory', 0, 1) WITH NOWAIT; - - SELECT @MinServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'min server memory (MB)'; - SELECT @MaxServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'max server memory (MB)'; - - IF (@MinServerMemory = @MaxServerMemory) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 190) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES - ( 190, - 200, - 'Performance', - 'Non-Dynamic Memory', - 'https://BrentOzar.com/go/memory', - 'Minimum Server Memory setting is the same as the Maximum (both set to ' + CAST(@MinServerMemory AS NVARCHAR(50)) + '). This will not allow dynamic memory. Please revise memory settings' - ); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 188 ) - BEGIN - - /* Let's set variables so that our query is still SARGable */ - - IF @Debug IN (1, 2) RAISERROR('Setting @Processors.', 0, 1) WITH NOWAIT; - - SET @Processors = (SELECT cpu_count FROM sys.dm_os_sys_info); - - IF @Debug IN (1, 2) RAISERROR('Setting @NUMANodes', 0, 1) WITH NOWAIT; - - SET @NUMANodes = (SELECT COUNT(1) - FROM sys.dm_os_performance_counters pc - WHERE pc.object_name LIKE '%Buffer Node%' - AND counter_name = 'Page life expectancy'); - /* If Cost Threshold for Parallelism is default then flag as a potential issue */ - /* If MAXDOP is default and processors > 8 or NUMA nodes > 1 then flag as potential issue */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 188) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 188 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/cxpacket' AS URL , - ( 'Set to ' + CAST(cr.value_in_use AS NVARCHAR(50)) + ', its default value. Changing this sp_configure setting may reduce CXPACKET waits.') - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - AND cr.value_in_use = cd.DefaultValue - WHERE cr.name = 'cost threshold for parallelism' - OR (cr.name = 'max degree of parallelism' AND (@NUMANodes > 1 OR @Processors > 8)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 24 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 24) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 24 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'System Database on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) IN ( 'master', - 'model', 'msdb' ); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 25 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 25) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 25 AS CheckID , - 'tempdb' , - 20 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - CASE WHEN growth > 0 - THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) - ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) - END AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) = 'tempdb'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 26 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 26) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 26 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 20 AS Priority , - 'Reliability' AS FindingsGroup , - 'User Databases on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) NOT IN ( 'master', - 'model', 'msdb', - 'tempdb' ) - AND DB_NAME(database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 26 ); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 27 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 27) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 'master' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Master Database' AS Finding , - 'https://BrentOzar.com/go/mastuser' AS URL , - ( 'The ' + name - + ' table in the master database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. 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; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 28 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 28) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 28 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the MSDB Database' AS Finding , - 'https://BrentOzar.com/go/msdbuser' AS URL , - ( 'The ' + name - + ' table in the msdb database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the msdb database may not be restored in the event of a disaster.' ) AS Details - FROM msdb.sys.tables - WHERE is_ms_shipped = 0 AND name NOT LIKE '%DTA_%'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 29 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 29) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 29 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Model Database' AS Finding , - 'https://BrentOzar.com/go/model' AS URL , - ( 'The ' + name - + ' table in the model database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the model database are automatically copied into all new databases.' ) AS Details - FROM model.sys.tables - WHERE is_ms_shipped = 0; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 30 ) - BEGIN - IF ( SELECT COUNT(*) - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 - ) < 7 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 30) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 30 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Not All Alerts Configured' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - END; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 59 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE enabled = 1 - AND COALESCE(has_notification, 0) = 0 - AND (job_id IS NULL OR job_id = 0x)) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 59) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 59 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Configured without Follow Up' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 96 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE message_id IN ( 823, 824, 825 ) ) - - BEGIN; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 96) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 96 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Corruption' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; - - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 61 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 61) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 61 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Sev 19-25' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; - - END; - - END; - - --check for disabled alerts - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 98 ) - BEGIN - IF EXISTS ( SELECT name - FROM msdb.dbo.sysalerts - WHERE enabled = 0 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 98) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 98 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Disabled' AS Finding , - 'https://www.BrentOzar.com/go/alerts/' AS URL , - ( 'The following Alert is disabled, please review and enable if desired: ' - + name ) AS Details - FROM msdb.dbo.sysalerts - WHERE enabled = 0; - - END; - - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 31 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysoperators - WHERE enabled = 1 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 31) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 31 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Operators Configured/Enabled' AS Finding , - 'https://BrentOzar.com/go/op' AS URL , - ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - - END; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 34 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_db_mirroring_auto_page_repair' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 34) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 34 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details - FROM (SELECT rp2.database_id, rp2.modification_time - FROM sys.dm_db_mirroring_auto_page_repair rp2 - WHERE rp2.[database_id] not in ( - SELECT db2.[database_id] - FROM sys.databases as db2 - WHERE db2.[state] = 1 - ) ) as rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 89 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_hadr_auto_page_repair' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 89) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 89 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''AlwaysOn has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details - FROM sys.dm_hadr_auto_page_repair rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 90 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.sys.all_objects - WHERE name = 'suspect_pages' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 90) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 90 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details - FROM msdb.dbo.suspect_pages sp - INNER JOIN master.sys.databases db ON sp.database_id = db.database_id - WHERE sp.last_update_date >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 36 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 36) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 36 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Reads on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Reads are averaging longer than 200ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_read_ms / ( 1.0 + num_of_reads ) ) > 200 - AND num_of_reads > 100000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 37 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 37) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 37 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Writes on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Writes are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_write_ms / ( 1.0 - + num_of_writes ) ) > 100 - AND num_of_writes > 100000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 40 ) - BEGIN - IF ( SELECT COUNT(*) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - ) = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 40) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 40 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Only Has 1 Data File' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' - ); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 183 ) - - BEGIN - - IF ( SELECT COUNT (distinct [size]) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - 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; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 183 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Unevenly Sized Data Files' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB data files are not configured with the same size. Unevenly sized tempdb data files will result in unevenly sized workloads.' - ); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 44 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 44) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 44 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Order Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'order hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 45 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 45) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 45 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Join Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'join hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 49 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 49) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 49 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Linked Server Configured' AS Finding , - 'https://BrentOzar.com/go/link' AS URL , - +CASE WHEN l.remote_name = 'sa' - THEN s.data_source - + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' - ELSE s.data_source - + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' - END AS Details - FROM sys.servers s - INNER JOIN sys.linked_logins l ON s.server_id = l.server_id - WHERE s.is_linked = 1; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 50 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 50) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 50 AS CheckID , - 100 AS Priority , - ''Performance'' AS FindingsGroup , - ''Max Memory Set Too High'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''SQL Server max memory is set to '' - + CAST(c.value_in_use AS VARCHAR(20)) - + '' megabytes, but the server only has '' - + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes. SQL Server may drain the system dry of memory, and under certain conditions, this can cause Windows to swap to disk.'' AS Details - FROM sys.dm_os_sys_memory m - INNER JOIN sys.configurations c ON c.name = ''max server memory (MB)'' - WHERE CAST(m.total_physical_memory_kb AS BIGINT) < ( CAST(c.value_in_use AS BIGINT) * 1024 ) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 51 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 51) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 51 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details - FROM sys.dm_os_sys_memory m - WHERE CAST(m.available_physical_memory_kb AS BIGINT) < 262144 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 159 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 159) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 159 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low in NUMA Nodes'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details - FROM sys.dm_os_nodes m - WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%'' 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 53 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 53) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 53 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Cluster Node' AS Finding , - 'https://BrentOzar.com/go/node' AS URL , - 'This is a node in a cluster.' AS Details - FROM sys.dm_os_cluster_nodes; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 55 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 55 AS CheckID , - [name] AS DatabaseName , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Database Owner <> SA' AS Finding , - 'https://BrentOzar.com/go/owndb' AS URL , - ( 'Database name: ' + [name] + ' ' - + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details - FROM sys.databases - WHERE SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01) - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 55); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 57 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 57) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 57 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'SQL Agent Job Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Job [' + j.name - + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details - FROM msdb.dbo.sysschedules sched - JOIN msdb.dbo.sysjobschedules jsched ON sched.schedule_id = jsched.schedule_id - JOIN msdb.dbo.sysjobs j ON jsched.job_id = j.job_id - WHERE sched.freq_type = 64 - AND sched.enabled = 1; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 97 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 97) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 97 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Unusual SQL Server Edition' AS Finding , - 'https://BrentOzar.com/go/workgroup' AS URL , - ( 'This server is using ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + ', which is capped at low amounts of CPU and memory.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Data Center%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Business Intelligence%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 154 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 154) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 154 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - '32-bit SQL Server Installed' AS Finding , - 'https://BrentOzar.com/go/32bit' AS URL , - ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 62 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 62) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 62 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Old Compatibility Level' AS Finding , - 'https://BrentOzar.com/go/compatlevel' AS URL , - ( 'Database ' + [name] - + ' is compatibility level ' - + CAST(compatibility_level AS VARCHAR(20)) - + ', which may cause unwanted results when trying to run queries that have newer T-SQL features.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 62) - AND compatibility_level <= 90; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 94 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 94 AS CheckID , - 200 AS [Priority] , - 'Monitoring' AS FindingsGroup , - 'Agent Jobs Without Failure Emails' AS Finding , - 'https://BrentOzar.com/go/alerts' AS URL , - 'The job ' + [name] - + ' has not been set up to notify an operator if it fails.' AS Details - FROM msdb.[dbo].[sysjobs] j - INNER JOIN ( SELECT DISTINCT - [job_id] - FROM [msdb].[dbo].[sysjobschedules] - WHERE next_run_date > 0 - ) s ON j.job_id = s.job_id - WHERE j.enabled = 1 - AND j.notify_email_operator_id = 0 - AND j.notify_netsend_operator_id = 0 - AND j.notify_page_operator_id = 0 - AND j.category_id <> 100; /* Exclude SSRS category */ - END; - - - IF EXISTS ( SELECT 1 - FROM sys.configurations - WHERE name = 'remote admin connections' - AND value_in_use = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 100 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 100) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 100 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Remote DAC Disabled' AS Finding , - 'https://BrentOzar.com/go/dac' AS URL , - 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.'; - END; - - - IF EXISTS ( SELECT * - FROM sys.dm_os_schedulers - WHERE is_online = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 101 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 101) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 101 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'CPU Schedulers Offline' AS Finding , - 'https://BrentOzar.com/go/schedulers' AS URL , - 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 110 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_os_memory_nodes') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 110) WITH NOWAIT; - - SET @StringToExecute = 'IF EXISTS (SELECT * - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - WHERE n.node_state_desc = ''OFFLINE'') - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 110 AS CheckID , - 50 AS Priority , - ''Performance'' AS FindingGroup , - ''Memory Nodes Offline'' AS Finding , - ''https://BrentOzar.com/go/schedulers'' AS URL , - ''Due to affinity masking or licensing problems, some of the memory may not be available.'' 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF EXISTS ( SELECT * - FROM sys.databases - WHERE state > 1 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 102 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 102) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 102 AS CheckID , - [name] , - 20 AS Priority , - 'Reliability' AS FindingGroup , - 'Unusual Database State: ' + [state_desc] AS Finding , - 'https://BrentOzar.com/go/repair' AS URL , - 'This database may not be online.' - FROM sys.databases - WHERE state > 1; - END; - - IF EXISTS ( SELECT * - FROM master.sys.extended_procedures ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 105 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 105) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 105 AS CheckID , - 'master' , - 200 AS Priority , - 'Reliability' AS FindingGroup , - 'Extended Stored Procedures in Master' AS Finding , - 'https://BrentOzar.com/go/clr' AS URL , - 'The [' + name - + '] extended stored procedure is in the master database. CLR may be in use, and the master database now needs to be part of your backup/recovery planning.' - FROM master.sys.extended_procedures; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 107 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 107) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 107 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: ' + wait_type AS Finding , - 'https://BrentOzar.com/go/poison/#' + wait_type AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') - GROUP BY wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 121 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 121) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 121 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: Serializable Locking' AS Finding , - 'https://BrentOzar.com/go/serializable' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - - IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 162 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 162 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: CMEMTHREAD & NUMA' AS Finding , - 'https://BrentOzar.com/go/poison' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' - FROM sys.dm_os_nodes n - INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' - WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 - AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') - GROUP BY w.wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 111 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 111) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - DatabaseName , - URL , - Details - ) - SELECT 111 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Possibly Broken Log Shipping' AS Finding , - d.[name] , - 'https://BrentOzar.com/go/shipping' AS URL , - d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' - FROM [master].sys.databases d - INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id - AND dm.mirroring_role IS NULL - WHERE ( d.[state] = 1 - OR (d.[state] = 0 AND d.[is_in_standby] = 1) ) - AND NOT EXISTS(SELECT * FROM msdb.dbo.restorehistory rh - INNER JOIN msdb.dbo.backupset bs ON rh.backup_set_id = bs.backup_set_id - WHERE d.[name] COLLATE SQL_Latin1_General_CP1_CI_AS = rh.destination_database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND rh.restore_date >= DATEADD(dd, -2, GETDATE())); - - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 112 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'change_tracking_databases') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 112) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 112 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Change Tracking Enabled'' AS Finding, - ''https://BrentOzar.com/go/tracking'' AS URL, - ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 116 ) - AND EXISTS (SELECT * FROM msdb.sys.all_columns WHERE name = 'compressed_backup_size') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 116) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 116 AS CheckID , - 200 AS Priority , - ''Informational'' AS FindingGroup , - ''Backup Compression Default Off'' AS Finding , - ''https://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.'' - 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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 117 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_exec_query_resource_semaphores') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 117) WITH NOWAIT; - - SET @StringToExecute = 'IF 0 < (SELECT SUM([forced_grant_count]) FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL) - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 117 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Memory Pressure Affecting Queries'' AS Finding, - ''https://BrentOzar.com/go/grants'' AS URL, - CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' - FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT 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.'; - - EXECUTE(@StringToExecute); - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 124 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 124) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 124, 150, 'Performance', 'Deadlocks Happening Daily', 'https://BrentOzar.com/go/deadlocks', - CAST(p.cntr_value AS NVARCHAR(100)) + ' deadlocks recorded since startup. To find them, run sp_BlitzLock.' AS Details - FROM sys.dm_os_performance_counters p - INNER JOIN sys.databases d ON d.name = 'tempdb' - WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' - AND RTRIM(p.instance_name) = '_Total' - AND p.cntr_value > 0 - AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; - END; - - - IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 125 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 125) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://BrentOzar.com/askbrent/plan-cache-erased-recently/', - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' - FROM sys.dm_exec_query_stats WITH (NOLOCK) - ORDER BY creation_time; - END; - - IF EXISTS (SELECT * FROM sys.configurations WHERE name = 'priority boost' AND (value = 1 OR value_in_use = 1)) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 126 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 126) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://BrentOzar.com/go/priorityboost/', - 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.'); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 128 ) - BEGIN - - IF (@ProductVersionMajor = 12 AND @ProductVersionMinor < 5000) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor < 6020) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 9 /*AND @ProductVersionMinor <= 5000*/) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 128) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://BrentOzar.com/go/unsupported', - 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + - CASE WHEN @ProductVersionMajor > 9 THEN - CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' - ELSE ' is no longer support by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); - END; - - END; - - /* Reliability - Dangerous Build of SQL Server (Corruption) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 129 ) - BEGIN - IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2342) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 129) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(129, 20, 'Reliability', 'Dangerous Build of SQL Server (Corruption)', 'http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; - - END; - - /* Reliability - Dangerous Build of SQL Server (Security) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 157 ) - BEGIN - IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4000 AND @ProductVersionMinor <= 4017) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4251 AND @ProductVersionMinor <= 4319) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3129) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3300 AND @ProductVersionMinor <= 3447) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2253) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2300 AND @ProductVersionMinor <= 2370) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 157) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(157, 20, 'Reliability', 'Dangerous Build of SQL Server (Security)', 'https://technet.microsoft.com/en-us/library/security/MS14-044', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; - - END; - - /* Check if SQL 2016 Standard Edition but not SP1 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 189 ) - BEGIN - IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 189) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(189, 100, 'Features', 'Missing Features', 'https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-server-2016-service-pack-1-sp1-released/', - 'SQL 2016 Standard Edition is being used but not Service Pack 1. Check the URL for a list of Enterprise Features that are included in Standard Edition as of SP1.'); - END; - - END; - - /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 145 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 145) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 145 AS CheckID, - 10 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) - OR SUM(mem.pages_kb / 1024.0) > 250000 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - In-Memory OLTP (Hekaton) In Use */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 146 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 146) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 146 AS CheckID, - 200 AS Priority, - ''Performance'' AS FindingsGroup, - ''In-Memory OLTP (Hekaton) In Use'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING SUM(mem.pages_kb / 1024.0) > 10 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.'; - - EXECUTE(@StringToExecute); - END; - - /* In-Memory OLTP (Hekaton) - Transaction Errors */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 147 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_xtp_transaction_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 147) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 147 AS CheckID, - 100 AS Priority, - ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, - ''Transaction Errors'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details - FROM sys.dm_xtp_transaction_stats - WHERE validation_failures <> 0 - OR dependencies_failed <> 0 - OR write_conflicts <> 0 - OR unique_constraint_violations <> 0 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.'; - - EXECUTE(@StringToExecute); - END; - - - - /* Reliability - Database Files on Network File Shares */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 148 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 148) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 148 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files on Network File Shares' AS Finding , - 'https://BrentOzar.com/go/nas' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE '\\%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 148); - END; - - /* Reliability - Database Files Stored in Azure */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 149 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 149) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 149 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files Stored in Azure' AS Finding , - 'https://BrentOzar.com/go/azurefiles' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE 'http://%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 149); - END; - - - /* Reliability - Errors Logged Recently in the Default Trace */ - - /* First, let's check that there aren't any issues with the trace files */ - BEGIN TRY - - INSERT INTO #fnTraceGettable - ( TextData , - DatabaseName , - EventClass , - Severity , - StartTime , - EndTime , - Duration , - NTUserName , - NTDomainName , - HostName , - ApplicationName , - LoginName , - DBUserName - ) - SELECT TOP 20000 - CONVERT(NVARCHAR(4000),t.TextData) , - t.DatabaseName , - t.EventClass , - t.Severity , - t.StartTime , - t.EndTime , - t.Duration , - t.NTUserName , - t.NTDomainName , - t.HostName , - t.ApplicationName , - t.LoginName , - t.DBUserName - FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t - WHERE - ( - t.EventClass = 22 - AND t.Severity >= 17 - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - ) - OR - ( - t.EventClass IN (92, 93) - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - AND t.Duration > 15000000 - ) - OR - ( - t.EventClass IN (94, 95, 116) - ) - - SET @TraceFileIssue = 0 - - END TRY - BEGIN CATCH - - SET @TraceFileIssue = 1 - - END CATCH - - IF @TraceFileIssue = 1 - BEGIN - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 199 ) - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - '199' AS CheckID , - '' AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'There Is An Error With The Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - 'Somebody has been messing with your trace files. Check the files are present at ' + @base_tracefilename AS Details - END - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 150 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 150) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 150 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Errors Logged Recently in the Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - CAST(t.TextData AS NVARCHAR(4000)) AS Details - FROM #fnTraceGettable t - WHERE t.EventClass = 22 - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.Severity >= 17 - --AND t.StartTime > DATEADD(dd, -30, GETDATE()); - END; - - - /* Performance - File Growths Slow */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 151 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 151) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 151 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'File Growths Slow' AS Finding , - 'https://BrentOzar.com/go/filegrowth' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting file autogrowth to a smaller increment.' AS Details - FROM #fnTraceGettable t - WHERE t.EventClass IN (92, 93) - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.StartTime > DATEADD(dd, -30, GETDATE()) - --AND t.Duration > 15000000 - GROUP BY t.DatabaseName - HAVING COUNT(*) > 1; - END; - - - /* Performance - Many Plans for One Query */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 160 ) - AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 160) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 160 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Many Plans for One Query'' AS Finding, - ''https://BrentOzar.com/go/parameterization'' AS URL, - CAST(COUNT(DISTINCT plan_handle) AS NVARCHAR(50)) + '' plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa - WHERE pa.attribute = ''dbid'' - GROUP BY qs.query_hash, pa.value - HAVING COUNT(DISTINCT plan_handle) > 50 - ORDER BY COUNT(DISTINCT plan_handle) DESC 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - High Number of Cached Plans */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 161 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 161) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 161 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Number of Cached Plans'' AS Finding, - ''https://BrentOzar.com/go/planlimits'' AS URL, - ''Your server configuration is limited to '' + CAST(ht.buckets_count * 4 AS VARCHAR(20)) + '' '' + ht.name + '', and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details - FROM sys.dm_os_memory_cache_hash_tables ht - INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type - where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) - AND cc.entries_count >= (3 * ht.buckets_count) 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - Too Much Free Memory */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 165 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 165) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://BrentOzar.com/go/freememory', - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool, which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - END; - - - /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 155 ) - AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 155) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 155 AS CheckID , - 0 AS Priority , - 'Outdated sp_Blitz' AS FindingsGroup , - 'sp_Blitz is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details; - END; - - - /* Populate a list of database defaults. I'm doing this kind of oddly - - it reads like a lot of work, but this way it compiles & runs on all - versions of SQL Server. - */ - - IF @Debug IN (1, 2) RAISERROR('Generating database defaults.', 0, 1) WITH NOWAIT; - - INSERT INTO #DatabaseDefaults - SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_read_committed_snapshot_on', 0, 133, 210, 'Read Committed Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); - /* Not alerting for this since we actually want it and we have a separate check for it: - INSERT INTO #DatabaseDefaults - SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); - */ - INSERT INTO #DatabaseDefaults - SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); - - DECLARE DatabaseDefaultsLoop CURSOR FOR - SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details - FROM #DatabaseDefaults; - - OPEN DatabaseDefaultsLoop; - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - WHILE @@FETCH_STATUS = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, @CurrentCheckID) WITH NOWAIT; - - /* Target Recovery Time (142) can be either 0 or 60 due to a number of bugs */ - IF @CurrentCheckID = 142 - 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.[' + @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.[' + @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.'; - - EXEC (@StringToExecute); - - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - END; - - CLOSE DatabaseDefaultsLoop; - DEALLOCATE DatabaseDefaultsLoop; - - -/*This checks to see if Agent is Offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 167 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 167) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 167 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Agent is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Server Agent%' - AND CAST(SERVERPROPERTY('Edition') AS VARCHAR(1000)) NOT LIKE '%xpress%'; - - END; - END; - -/*This checks to see if the Full Text thingy is offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 168 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 168 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; - - END; - END; - -/*This checks which service account SQL Server is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 169 ) - - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 169) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 169 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server%' - AND [servicename] NOT LIKE 'SQL Server Agent%'; - - END; - END; - -/*This checks which service account SQL Agent is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 170 ) - - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 170) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 170 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server Agent is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server Agent%'; - - END; - END; - -/*This counts memory dumps and gives min and max date of in view*/ -IF @ProductVersionMajor >= 10 - AND NOT (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 4297) /* Skip due to crash bug: https://support.microsoft.com/en-us/help/2908087 */ - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 171 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_memory_dumps' ) - BEGIN - IF 5 <= (SELECT COUNT(*) FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 171) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 171 AS [CheckID] , - 20 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Memory Dumps Have Occurred' AS [Finding] , - 'https://BrentOzar.com/go/dump' AS [URL] , - ( 'That ain''t good. I''ve had ' + - CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + - CAST(CAST(MIN([creation_time]) AS DATETIME) AS VARCHAR(100)) + - ' and ' + - CAST(CAST(MAX([creation_time]) AS DATETIME) AS VARCHAR(100)) + - '!' - ) AS [Details] - FROM - [sys].[dm_server_memory_dumps] - WHERE [creation_time] >= DATEADD(year, -1, GETDATE()); - - END; - END; - END; - -/*Checks to see if you're on Developer or Evaluation*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 173 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 173) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 173 AS [CheckID] , - 200 AS [Priority] , - 'Licensing' AS [FindingsGroup] , - 'Non-Production License' AS [Finding] , - 'https://BrentOzar.com/go/licensing' AS [URL] , - ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + - CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + - ' the good folks at Microsoft might get upset with you. Better start counting those cores.' - ) AS [Details] - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Developer%' - OR CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Evaluation%'; - - END; - -/*Checks to see if Buffer Pool Extensions are in use*/ - IF @ProductVersionMajor >= 12 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 174 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 174) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 174 AS [CheckID] , - 200 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Buffer Pool Extensions Enabled' AS [Finding] , - 'https://BrentOzar.com/go/bpe' AS [URL] , - ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + - [path] + - '. It''s currently ' + - CASE WHEN [current_size_in_kb] / 1024. / 1024. > 0 - THEN CAST([current_size_in_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([current_size_in_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + - '. Did you know that BPEs only provide single threaded access 8KB (one page) at a time?' - ) AS [Details] - FROM sys.dm_os_buffer_pool_extension_configuration - WHERE [state_description] <> 'BUFFER POOL EXTENSION DISABLED'; - - END; - -/*Check for too many tempdb files*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 175 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 175) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 175 AS CheckID , - 'TempDB' AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB Has >16 Data Files' AS Finding , - 'https://BrentOzar.com/go/tempdb' AS URL , - 'Woah, Nelly! TempDB has ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + '. Did you forget to terminate a loop somewhere?' AS Details - FROM sys.[master_files] AS [mf] - WHERE [mf].[database_id] = 2 AND [mf].[type] = 0 - HAVING COUNT_BIG(*) > 16; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 176 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_xe_sessions' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 176) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 176 AS CheckID , - '' AS DatabaseName , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Extended Events Hyperextension' AS Finding , - 'https://BrentOzar.com/go/xe' AS URL , - 'Hey big spender, you have ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + ' Extended Events sessions running. You sure you meant to do that?' AS Details - FROM sys.dm_xe_sessions - WHERE [name] NOT IN - ( 'AlwaysOn_health', 'system_health', 'telemetry_xevents', 'sp_server_diagnostics', 'hkenginexesession' ) - AND name NOT LIKE '%$A%' - HAVING COUNT_BIG(*) >= 2; - END; - END; - - /*Harmful startup parameter*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 177 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_registry' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 177) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 177 AS CheckID , - '' AS DatabaseName , - 5 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Disabled Internal Monitoring Features' AS Finding , - 'https://msdn.microsoft.com/en-us/library/ms190737.aspx' AS URL , - 'You have -x as a startup parameter. You should head to the URL and read more about what it does to your system.' AS Details - FROM - [sys].[dm_server_registry] AS [dsr] - WHERE - [dsr].[registry_key] LIKE N'%MSSQLServer\Parameters' - AND [dsr].[value_data] = '-x';; - END; - END; - - - /* Reliability - Dangerous Third Party Modules - 179 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 179 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 179) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 179 AS [CheckID] , - 5 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Dangerous Third Party Modules' AS [Finding] , - 'https://support.microsoft.com/en-us/kb/2033238' AS [URL] , - ( COALESCE(company, '') + ' - ' + COALESCE(description, '') + ' - ' + COALESCE(name, '') + ' - suspected dangerous third party module is installed.') AS [Details] - FROM sys.dm_os_loaded_modules - WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') /* McAfee VirusScan Enterprise */ - OR UPPER(name) LIKE UPPER('%\HIPI.DLL') OR UPPER(name) LIKE UPPER('%\HcSQL.dll') OR UPPER(name) LIKE UPPER('%\HcApi.dll') OR UPPER(name) LIKE UPPER('%\HcThe.dll') /* McAfee Host Intrusion */ - OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') /* Sophos AV */ - OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL'); /* OSISoft PI data access */ - - END; - - /*Find shrink database tasks*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 180 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 180) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , [id] -- ID required to link maintenace plan with jobs and jobhistory (sp_Blitz Issue #776) - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ) - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 180 AS [CheckID] , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS [FindingsGroup] , - 'Shrink Database Step In Maintenance Plan' AS [Finding] , - 'https://BrentOzar.com/go/autoshrink' AS [URL] , - 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS [Details] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - join msdb.dbo.sysmaintplan_subplans as sms - on mps.id = sms.plan_id - JOIN msdb.dbo.sysjobs j - on sms.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobsteps AS step - ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE [c].[value]('(@dts:ObjectName)', 'VARCHAR(128)') = 'Shrink Database Task'; - - END; - - - /*Find repetitive maintenance tasks*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 181 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 181) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ), [maintenance_plan_table] AS ( - SELECT [mps].[name] - ,[c].[value]('(@dts:ObjectName)', 'NVARCHAR(128)') AS [step_name] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - ), [mp_steps_pretty] AS (SELECT DISTINCT [m1].[name] , - STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] - FOR XML PATH(N'')), 1, 2, N'') AS [maintenance_plan_steps] - FROM [maintenance_plan_table] AS [m1]) - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 181 AS [CheckID] , - 100 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Repetitive Steps In Maintenance Plans' AS [Finding] , - 'https://ola.hallengren.com/' AS [URL] , - 'The maintenance plan ' + [m].[name] + ' is doing repetitive work on indexes and statistics. Perhaps it''s time to try something more modern?' AS [Details] - FROM [mp_steps_pretty] m - WHERE m.[maintenance_plan_steps] LIKE '%Rebuild%Reorganize%' - OR m.[maintenance_plan_steps] LIKE '%Rebuild%Update%'; - - END; - - - /* Reliability - No Failover Cluster Nodes Available - 184 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '10%' - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '9%' - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 - 184 AS CheckID , - 20 AS Priority , - ''Reliability'' AS FindingsGroup , - ''No Failover Cluster Nodes Available'' AS Finding , - ''https://BrentOzar.com/go/node'' AS URL , - ''There are no failover cluster nodes available if the active node fails'' AS Details - FROM ( - SELECT SUM(CASE WHEN [status] = 0 AND [is_current_owner] = 0 THEN 1 ELSE 0 END) AS [available_nodes] - FROM sys.dm_os_cluster_nodes - ) a - WHERE [available_nodes] < 1 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.'; - - EXECUTE(@StringToExecute); - END; - - /* Reliability - TempDB File Error */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 191 ) - AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> (SELECT COUNT(*) FROM tempdb.sys.database_files) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 191) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 191 AS [CheckID] , - 50 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'TempDB File Error' AS [Finding] , - 'https://BrentOzar.com/go/tempdboops' AS [URL] , - 'Mismatch between the number of TempDB files in sys.master_files versus tempdb.sys.database_files' AS [Details]; - END; - -/*Perf - Odd number of cores in a socket*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID = 198 ) - AND EXISTS ( SELECT 1 - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 198) WITH NOWAIT - - INSERT INTO #BlitzResults - ( - CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details - ) - SELECT 198 AS CheckID, - NULL AS DatabaseName, - 10 AS Priority, - 'Performance' AS FindingsGroup, - 'CPU w/Odd Number of Cores' AS Finding, - 'https://BrentOzar.com/go/oddity' AS URL, - 'Node ' + CONVERT(VARCHAR(10), parent_node_id) + ' has ' + CONVERT(VARCHAR(10), COUNT(cpu_id)) - + CASE WHEN COUNT(cpu_id) = 1 THEN ' core assigned to it. This is a really bad NUMA configuration.' - ELSE ' cores assigned to it. This is a really bad NUMA configuration.' - END AS Details - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; - - END; - -/*Begin: checking default trace for odd DBCC activity*/ - - --Grab relevant event data - IF @TraceFileIssue = 0 - BEGIN - SELECT UPPER( - REPLACE( - SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, - ISNULL( - NULLIF( - CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), - 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. - , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ) - , '') --This replaces any optional WITH clause to a DBCC command, like tableresults. - ) AS [dbcc_event_trunc_upper], - UPPER( - REPLACE( - CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ), '')) AS [dbcc_event_full_upper], - MIN(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS min_start_time, - MAX(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS max_start_time, - t.NTUserName AS [nt_user_name], - t.NTDomainName AS [nt_domain_name], - t.HostName AS [host_name], - t.ApplicationName AS [application_name], - t.LoginName [login_name], - t.DBUserName AS [db_user_name] - INTO #dbcc_events_from_trace - FROM #fnTraceGettable AS t - WHERE t.EventClass = 116 - OPTION(RECOMPILE) - END; - - /*Overall count of DBCC events excluding silly stuff*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 203 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 199) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 203 AS CheckID , - 50 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'Overall Events' AS Finding , - '' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This does not include CHECKDB and other usually benign DBCC events.' - AS Details - FROM #dbcc_events_from_trace d - /* This WHERE clause below looks horrible, but it's because users can run stuff like - DBCC LOGINFO - with lots of spaces (or carriage returns, or comments) in between the DBCC and the - command they're trying to run. See Github issues 1062, 1074, 1075. - */ - WHERE d.dbcc_event_full_upper NOT LIKE '%DBCC%ADDINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKALLOC%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCATALOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCONSTRAINTS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKDB%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKFILEGROUP%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKIDENT%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKPRIMARYFILE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CLEANTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%DBINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%ERRORLOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INCREMENTINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INPUTBUFFER%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%LOGINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%OPENTRAN%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SETINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOWFILESTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOW_STATISTICS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%NETSTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%LOGSPACE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEON%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEOFF%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACESTATUS%' - AND d.application_name NOT LIKE 'Critical Care(R) Collector' - AND d.application_name NOT LIKE '%Red Gate Software Ltd SQL Prompt%' - AND d.application_name NOT LIKE '%Spotlight Diagnostic Server%' - AND d.application_name NOT LIKE '%SQL Diagnostic Manager%' - AND d.application_name NOT LIKE '%Sentry%' - - - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running drop clean buffers*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 207 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 200) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 207 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC DROPCLEANBUFFERS Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. If this is a production box, know that you''re clearing all data out of memory when this happens. What kind of monster would do that?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC DROPCLEANBUFFERS' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running free proc cache*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 208 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 201) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 208 AS CheckID , - 10 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'DBCC FREEPROCCACHE Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This has bad idea jeans written all over its butt, like most other bad idea jeans.' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC FREEPROCCACHE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone clearing wait stats*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 205 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 205 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'Wait Stats Cleared Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. Why are you clearing wait stats? What are you hiding?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR)' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone writing to pages. Yeah, right?*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 209 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 209 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'DBCC WRITEPAGE Used Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying to fix corruption, or cause corruption?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper = N'DBCC WRITEPAGE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 210 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 204) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT 210 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC SHRINK% Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying cause bad performance on purpose?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper LIKE N'DBCC SHRINK%' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - -/*End: checking default trace for odd DBCC activity*/ - - /*Begin check for autoshrink events*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 206 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT 206 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Ran Recently' AS Finding , - '' AS URL , - N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' - + CONVERT(NVARCHAR(10), COUNT(*)) - + N' auto shrink events between ' - + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) - + ' that lasted on average ' - + CONVERT(NVARCHAR(10), AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime))) - + ' seconds.' AS Details - FROM #fnTraceGettable AS t - WHERE t.EventClass IN (94, 95) - GROUP BY t.DatabaseName - HAVING AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime)) > 5; - - END; - - - IF @CheckUserDatabaseObjects = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Starting @CheckUserDatabaseObjects section.', 0, 1) WITH NOWAIT - - /* - But what if you need to run a query in every individual database? - Check out CheckID 99 below. Yes, it uses sp_MSforeachdb, and no, - we're not happy about that. sp_MSforeachdb is known to have a lot - of issues, like skipping databases sometimes. However, this is the - only built-in option that we have. If you're writing your own code - for database maintenance, consider Aaron Bertrand's alternative: - http://www.mssqltips.com/sqlservertip/2201/making-a-more-reliable-and-flexible-spmsforeachdb/ - We don't include that as part of sp_Blitz, of course, because - copying and distributing copyrighted code from others without their - written permission isn't a good idea. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 99 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; 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://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 - databases. We're not checking #SkipChecks here for each database to - see if we should run the check in this database. That means we may - still run a skipped check if it involves sp_MSforeachdb. We just - don't output those results in the last step. - */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 163 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') - BEGIN - /* --TOURSTOP03-- */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 163) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 163, - ''?'', - 200, - ''Performance'', - ''Query Store Disabled'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; - END; - - - IF @ProductVersionMajor >= 13 AND @ProductVersionMinor < 2149 --CU1 has the fix in it - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 182 ) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 182) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 - 182, - ''Server'', - 20, - ''Reliability'', - ''Query Store Cleanup Disabled'', - ''https://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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 41 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 41) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 41, - ''?'', - 170, - ''File Configuration'', - ''Multiple Log Files on One Drive'', - ''https://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 ''?'' <> ''[tempdb]'' - GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 42 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 42) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 42, - ''?'', - 170, - ''File Configuration'', - ''Uneven File Growth Settings in One Filegroup'', - ''https://BrentOzar.com/go/grow'', - (''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'' - GROUP BY data_space_id - HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 82 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 82) WITH NOWAIT; - - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 82 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to percent'', - ''https://BrentOzar.com/go/percentgrowth'' AS URL, - ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CAST((CONVERT(BIGINT, f.size * 8) / 1000000) AS NVARCHAR(10)) + '' 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);'; - END; - - - - /* addition by Henrik Staun Poulsen, Stovi Software */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 158 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 158) WITH NOWAIT; - - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 158 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to 1MB'', - ''https://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((f.size * 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);'; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 33 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 33) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 33, - db_name(), - 200, - ''Licensing'', - ''Enterprise Edition Features In Use'', - ''https://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);'; - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 19 ) - BEGIN - /* Method 1: Check sys.databases parameters */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 19) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - - SELECT 19 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Replication In Use' AS Finding , - 'https://BrentOzar.com/go/repl' AS URL , - ( 'Database [' + [name] - + '] is a replication publisher, subscriber, or distributor.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 19) - AND is_published = 1 - OR is_subscribed = 1 - OR is_merge_published = 1 - OR is_distributor = 1; - - /* Method B: check subscribers for MSreplication_objects tables */ - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 19, - db_name(), - 200, - ''Informational'', - ''Replication In Use'', - ''https://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)'; - - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 32 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 32) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 32, - ''?'', - 150, - ''Performance'', - ''Triggers on Tables'', - ''https://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'' - HAVING SUM(1) > 0 OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 38 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 38) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 38, - ''?'', - 110, - ''Performance'', - ''Active Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that are being actively queried.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NOT NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 164 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'fn_validate_plan_guide') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 164) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 164, - ''?'', - 20, - ''Reliability'', - ''Plan Guides Failing'', - ''https://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)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 39 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 39) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 39, - ''?'', - 150, - ''Performance'', - ''Inactive Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that have not been queried since the last restart. These may be backup tables carelessly left behind.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 46 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 46) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 46, - ''?'', - 150, - ''Performance'', - ''Leftover Fake Indexes From Wizards'', - ''https://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);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 47 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 47) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 47, - ''?'', - 100, - ''Performance'', - ''Indexes Disabled'', - ''https://BrentOzar.com/go/ixoff'', - (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1 OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 48 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 48) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 48, - ''?'', - 150, - ''Performance'', - ''Foreign Keys Not Trusted'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 56 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 56) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 56, - ''?'', - 150, - ''Performance'', - ''Check Constraint Not Trusted'', - ''https://BrentOzar.com/go/trust'', - (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') - from [?].sys.check_constraints 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 OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 95 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 95) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 95 AS CheckID, - ''?'' as DatabaseName, - 110 AS Priority, - ''Performance'' AS FindingsGroup, - ''Plan Guides Enabled'' AS Finding, - ''https://BrentOzar.com/go/guides'' AS URL, - (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details - FROM [?].sys.plan_guides WHERE is_disabled = 0 OPTION (RECOMPILE);'; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 60 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 60) WITH NOWAIT; - - EXEC sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 60 AS CheckID, - ''?'' as DatabaseName, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Fill Factor Changed'', - ''https://BrentOzar.com/go/fillfactor'' AS URL, - ''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' objects with fill factor = '' + CAST(fill_factor AS NVARCHAR(5)) + ''%. This can cause memory and storage performance problems, but may also prevent page splits.'' - FROM [?].sys.indexes - WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 - GROUP BY fill_factor OPTION (RECOMPILE);'; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 78 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 78) WITH NOWAIT; - - EXECUTE master.sys.sp_MSforeachdb 'USE [?]; - INSERT INTO #Recompile - SELECT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA - FROM sys.sql_modules AS SM - LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() - LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' - LEFT OUTER JOIN INFORMATION_SCHEMA.ROUTINES AS ISR on ISR.Routine_Name = SO.name AND ISR.SPECIFIC_CATALOG = DB_Name() - WHERE SM.is_recompiled=1 OPTION (RECOMPILE); /* oh the rich irony of recompile here */ - '; - INSERT INTO #BlitzResults - (Priority, - FindingsGroup, - Finding, - DatabaseName, - URL, - Details, - CheckID) - SELECT [Priority] = '100', - FindingsGroup = 'Performance', - Finding = 'Stored Procedure WITH RECOMPILE', - DatabaseName = DBName, - URL = 'https://BrentOzar.com/go/recompile', - Details = '[' + DBName + '].[' + SPSchema + '].[' + ProcName + '] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.', - CheckID = '78' - FROM #Recompile AS TR WHERE ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%'; - DROP TABLE #Recompile; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 86 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 86) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://BrentOzar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM [?].dbo.sysmembers m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; - END; - - - /*Check for non-aligned indexes in partioned databases*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 72 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 72) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - insert into #partdb(dbname, objectname, type_desc) - SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc - FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id - JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id - LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() - WHERE o.type = ''u'' - -- Clustered and Non-Clustered indexes - AND i.type IN (1, 2) - AND o.object_id in - ( - SELECT a.object_id from - (SELECT ob.object_id, ds.type_desc from sys.objects ob JOIN sys.indexes ind on ind.object_id = ob.object_id join sys.data_spaces ds on ds.data_space_id = ind.data_space_id - GROUP BY ob.object_id, ds.type_desc ) a group by a.object_id having COUNT (*) > 1 - ) OPTION (RECOMPILE);'; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 72 AS CheckID , - dbname AS DatabaseName , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'The partitioned database ' + dbname - + ' may have non-aligned indexes' AS Finding , - 'https://BrentOzar.com/go/aligned' AS URL , - 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details - FROM #partdb - WHERE dbname IS NOT NULL - AND dbname NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 72); - DROP TABLE #partdb; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 113 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 113) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 113, - ''?'', - 50, - ''Reliability'', - ''Full Text Indexes Not Updating'', - ''https://BrentOzar.com/go/fulltext'', - (''At least one full text index in this database has not been crawled in the last week.'') - from [?].sys.fulltext_indexes i WHERE change_tracking_state_desc <> ''AUTO'' AND i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE()) OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 115 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 115) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 115, - ''?'', - 110, - ''Performance'', - ''Parallelism Rocket Surgery'', - ''https://BrentOzar.com/go/makeparallel'', - (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') - from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'' OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 122 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 122) WITH NOWAIT; - - /* SQL Server 2012 and newer uses temporary stats for AlwaysOn Availability Groups, and those show up as user-created */ - IF EXISTS (SELECT * - FROM sys.all_columns c - INNER JOIN sys.all_objects o ON c.object_id = o.object_id - WHERE c.name = 'is_temporary' AND o.name = 'stats') - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - - ELSE - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - - - END; /* IF NOT EXISTS ( SELECT 1 */ - - - /*Check for high VLF count: this will omit any database snapshots*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 69 ) - BEGIN - IF @ProductVersionMajor >= 11 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo2012 - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo2012 - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo2012;'; - DROP TABLE #LogInfo2012; - END; - ELSE - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (pre-2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo;'; - DROP TABLE #LogInfo; - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 80 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 80) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://BrentOzar.com/go/maxsize'', (''The ['' + DB_NAME() + ''] database file '' + name + '' has a max file size set to '' + CAST(CAST(max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') FROM sys.database_files WHERE max_size <> 268435456 AND max_size <> -1 AND type <> 2 AND name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; - END; - - - /* Check if columnstore indexes are in use - for Github issue #615 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ - BEGIN - TRUNCATE TABLE #TemporaryDatabaseResults; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; IF EXISTS(SELECT * FROM sys.indexes WHERE type IN (5,6)) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; - IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @ColumnStoreIndexesInUse = 1; - END; - - - /* Non-Default Database Scoped Config - Github issue #598 */ - IF EXISTS ( SELECT * FROM sys.all_objects WHERE [name] = 'database_scoped_configurations' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] through [%d].', 0, 1, 194, 197) 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; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) - SELECT def1.CheckID, DB_NAME(), 210, ''Non-Default Database Scoped Config'', dsc.[name], ''https://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 - LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (dsc.value = def.default_value 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);'; - END; - - - - - END; /* IF @CheckUserDatabaseObjects = 1 */ - - IF @CheckProcedureCache = 1 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Begin checking procedure cache', 0, 1) WITH NOWAIT; - - BEGIN - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 35 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 35) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 35 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Single-Use Plans in Procedure Cache' AS Finding , - 'https://BrentOzar.com/go/single' AS URL , - ( CAST(COUNT(*) AS VARCHAR(10)) - + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details - FROM sys.dm_exec_cached_plans AS cp - WHERE cp.usecounts = 1 - AND cp.objtype = 'Adhoc' - AND EXISTS ( SELECT - 1 - FROM sys.configurations - WHERE - name = 'optimize for ad hoc workloads' - AND value_in_use = 0 ) - HAVING COUNT(*) > 1; - END; - - - /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ - IF @@VERSION LIKE '%Microsoft SQL Server 2005%' - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - END; - IF @ProductVersionMajor >= 10 - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - /* Populate the query_plan_filtered field. Only works in 2005SP2+, but we're just doing it in 2008 to be safe. */ - UPDATE #dm_exec_query_stats - SET query_plan_filtered = qp.query_plan - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, - qs.statement_start_offset, - qs.statement_end_offset) - AS qp; - - END; - - /* Populate the additional query_plan, text, and text_filtered fields */ - UPDATE #dm_exec_query_stats - SET query_plan = qp.query_plan , - [text] = st.[text] , - text_filtered = SUBSTRING(st.text, - ( qs.statement_start_offset - / 2 ) + 1, - ( ( CASE qs.statement_end_offset - WHEN -1 - THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - - qs.statement_start_offset ) - / 2 ) + 1) - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) - AS qp; - - /* Dump instances of our own script. We're not trying to tune ourselves. */ - DELETE #dm_exec_query_stats - WHERE text LIKE '%sp_Blitz%' - OR text LIKE '%#BlitzResults%'; - - /* Look for implicit conversions */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 63 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 63) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 63 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%CONVERT_IMPLICIT%' - AND COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%PhysicalOp="Index Scan"%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 64 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 64) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 64 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion Affecting Cardinality' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 187 ) - - IF SERVERPROPERTY('IsHadrEnabled') = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 187) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 187 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Endpoints Owned by Users' AS [Finding] , - 'https://BrentOzar.com/go/owners' AS [URL] , - ( 'Endpoint ' + ep.[name] + ' is owned by ' + SUSER_NAME(ep.principal_id) + '. If the endpoint owner login is disabled or not available due to Active Directory problems, the high availability will stop working.' - ) AS [Details] - FROM sys.database_mirroring_endpoints ep - LEFT OUTER JOIN sys.dm_server_services s ON SUSER_NAME(ep.principal_id) = s.service_account - WHERE s.service_account IS NULL AND ep.principal_id <> 1; - END; - - /*Check for the last good DBCC CHECKDB date */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 68 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT #DBCCs - (ParentObject, - Object, - Field, - Value) - EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); - UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; - - WITH DB2 - AS ( SELECT DISTINCT - Field , - Value , - DbName - FROM #DBCCs - WHERE Field = 'dbi_dbccLastKnownGood' - ) - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 68 AS CheckID , - DB2.DbName AS DatabaseName , - 1 AS PRIORITY , - 'Reliability' AS FindingsGroup , - 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , - 'https://BrentOzar.com/go/checkdb' AS URL , - 'Last successful CHECKDB: ' - + CASE DB2.Value - WHEN '1900-01-01 00:00:00.000' - THEN ' never.' - ELSE DB2.Value - END AS Details - FROM DB2 - WHERE DB2.DbName <> 'tempdb' - AND DB2.DbName NOT IN ( SELECT DISTINCT - DatabaseName - FROM - #SkipChecks - WHERE CheckID IS NULL OR CheckID = 68) - AND DB2.DbName NOT IN ( SELECT name - FROM sys.databases - WHERE is_read_only = 1) - AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, - -14, - CURRENT_TIMESTAMP); - END; - - - - - /*Verify that the servername is set */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 70 ) - BEGIN - IF @@SERVERNAME IS NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - '@@Servername Not Set' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details; - END; - - IF /* @@SERVERNAME IS set */ - (@@SERVERNAME IS NOT NULL - AND - /* not a named instance */ - CHARINDEX('\',CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 - AND - /* not clustered, when computername may be different than the servername */ - SERVERPROPERTY('IsClustered') = 0 - AND - /* @@SERVERNAME is different than the computer name */ - @@SERVERNAME <> CAST(ISNULL(SERVERPROPERTY('ComputerNamePhysicalNetBIOS'),@@SERVERNAME) AS NVARCHAR(128)) ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Configuration' AS FindingsGroup , - '@@Servername Not Correct' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details; - END; - - END; - /*Check to see if a failsafe operator has been configured*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 73 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 73) WITH NOWAIT; - - DECLARE @AlertInfo TABLE - ( - FailSafeOperator NVARCHAR(255) , - NotificationMethod INT , - ForwardingServer NVARCHAR(255) , - ForwardingSeverity INT , - PagerToTemplate NVARCHAR(255) , - PagerCCTemplate NVARCHAR(255) , - PagerSubjectTemplate NVARCHAR(255) , - PagerSendSubjectOnly NVARCHAR(255) , - ForwardAlways INT - ); - INSERT INTO @AlertInfo - EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 73 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No failsafe operator configured' AS Finding , - 'https://BrentOzar.com/go/failsafe' AS URL , - ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details - FROM @AlertInfo - WHERE FailSafeOperator IS NULL; - END; - -/*Identify globally enabled trace flags*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - INSERT INTO #TraceStatus - EXEC ( ' DBCC TRACESTATUS(-1) WITH NO_INFOMSGS' - ); - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 74 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'TraceFlag On' AS Finding , - CASE WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN 'https://support.microsoft.com/en-us/kb/3210239' - ELSE'https://www.BrentOzar.com/go/traceflags/' END AS URL , - 'Trace flag ' + - CASE WHEN [T].[TraceFlag] = '2330' THEN ' 2330 enabled globally. Using this trace Flag disables missing index requests' - WHEN [T].[TraceFlag] = '1211' THEN ' 1211 enabled globally. Using this Trace Flag disables lock escalation when you least expect it. No Bueno!' - WHEN [T].[TraceFlag] = '1224' THEN ' 1224 enabled globally. Using this Trace Flag disables lock escalation based on the number of locks being taken. You shouldn''t have done that, Dave.' - WHEN [T].[TraceFlag] = '652' THEN ' 652 enabled globally. Using this Trace Flag disables pre-fetching during index scans. If you hate slow queries, you should turn that off.' - WHEN [T].[TraceFlag] = '661' THEN ' 661 enabled globally. Using this Trace Flag disables ghost record removal. Who you gonna call? No one, turn that thing off.' - WHEN [T].[TraceFlag] = '1806' THEN ' 1806 enabled globally. Using this Trace Flag disables instant file initialization. I question your sanity.' - WHEN [T].[TraceFlag] = '3505' THEN ' 3505 enabled globally. Using this Trace Flag disables Checkpoints. Probably not the wisest idea.' - WHEN [T].[TraceFlag] = '8649' THEN ' 8649 enabled globally. Using this Trace Flag drops cost thresholf for parallelism down to 0. I hope this is a dev server.' - WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN ' 834 is enabled globally. Using this Trace Flag with Columnstore Indexes is not a great idea.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN ' 8017 is enabled globally, which is the default for express edition.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN ' 8017 is enabled globally. Using this Trace Flag disables creation schedulers for all logical processors. Not good.' - ELSE [T].[TraceFlag] + ' is enabled globally.' END - AS Details - FROM #TraceStatus T; - END; - - /*Check for transaction log file larger than data file */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 75 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 75) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 75 AS CheckID , - DB_NAME(a.database_id) , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Transaction Log Larger than Data File' AS Finding , - 'https://BrentOzar.com/go/biglog' AS URL , - 'The database [' + DB_NAME(a.database_id) - + '] has a ' + CAST((CAST(a.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(20)) + ' GB transaction log file, larger than the total data file sizes. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details - FROM sys.master_files a - WHERE a.type = 1 - AND DB_NAME(a.database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID = 75 OR CheckID IS NULL) - AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ - AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) - FROM sys.master_files b - WHERE a.database_id = b.database_id - AND b.type = 0 - ) - AND a.database_id IN ( - SELECT database_id - FROM sys.databases - WHERE source_database_id IS NULL ); - END; - - /*Check for collation conflicts between user databases and tempdb */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 76 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 76) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 76 AS CheckID , - name AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Collation is ' + collation_name AS Finding , - 'https://BrentOzar.com/go/collate' AS URL , - 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details - FROM sys.databases - WHERE name NOT IN ( 'master', 'model', 'msdb') - AND name NOT LIKE 'ReportServer%' - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 76) - AND collation_name <> ( SELECT - collation_name - FROM - sys.databases - WHERE - name = 'tempdb' - ); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 77 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 77) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 77 AS CheckID , - dSnap.[name] AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Snapshot Online' AS Finding , - 'https://BrentOzar.com/go/snapshot' AS URL , - 'Database [' + dSnap.[name] - + '] is a snapshot of [' - + dOriginal.[name] - + ']. Make sure you have enough drive space to maintain the snapshot as the original database grows.' AS Details - FROM sys.databases dSnap - INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id - AND dSnap.name NOT IN ( - SELECT DISTINCT DatabaseName - FROM #SkipChecks - WHERE CheckID = 77 OR CheckID IS NULL); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 79 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 79) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 79 AS CheckID , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days OR Job is enabled AND Job Schedule is enabled - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS FindingsGroup , - 'Shrink Database Job' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - 'In the [' + j.[name] + '] job, step [' - + step.[step_name] - + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS Details - FROM msdb.dbo.sysjobs j - INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE step.command LIKE N'%SHRINKDATABASE%' - OR step.command LIKE N'%SHRINKFILE%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 81 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 81) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 81 AS CheckID , - 200 AS Priority , - 'Non-Active Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://www.BrentOzar.com/blitz/sp_configure/' AS URL , - ( 'This sp_configure option isn''t running under its set value. Its set value is ' - + CAST(cr.[value] AS VARCHAR(100)) - + ' and its running value is ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '. When someone does a RECONFIGURE or restarts the instance, this setting will start taking effect.' ) AS Details - FROM sys.configurations cr - WHERE cr.value <> cr.value_in_use - AND NOT (cr.name = 'min server memory (MB)' AND cr.value IN (0,16) AND cr.value_in_use IN (0,16)); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 123 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 123) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 123 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Agent Jobs Starting Simultaneously' AS Finding , - 'https://BrentOzar.com/go/busyagent/' AS URL , - ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details - FROM msdb.dbo.sysjobactivity - WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) - GROUP BY start_execution_date HAVING COUNT(*) > 1; - END; - - - IF @CheckServerInfo = 1 - BEGIN - -/*This checks Windows version. It would be better if Microsoft gave everything a separate build number, but whatever.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 172 ) - BEGIN - -- sys.dm_os_host_info includes both Windows and Linux info - IF EXISTS (SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Operating System Version' AS [Finding] , - ( CASE WHEN @IsWindowsOperatingSystem = 1 - THEN 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' - ELSE 'https://en.wikipedia.org/wiki/List_of_Linux_distributions' - END - ) AS [URL] , - ( CASE - WHEN [ohi].[host_platform] = 'Linux' THEN 'You''re running the ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ' distribution of ' + CAST([ohi].[host_platform] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' AND [ohi].[host_release] < '6' THEN 'You''re running a really old version: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] >= '6' AND [ohi].[host_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.2' THEN 'You''re running a rather modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - ELSE 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - END - ) AS [Details] - FROM [sys].[dm_os_host_info] [ohi]; - END; - ELSE - BEGIN - -- Otherwise, stick with Windows-only detection - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_windows_info' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Windows Version' AS [Finding] , - 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' AS [URL] , - ( CASE - WHEN [owi].[windows_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running a really old version: Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.2' THEN 'You''re running a rather modern version of Windows: Server 2012 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: Server 2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - ELSE 'I have no idea which version of Windows you''re on. Sorry.' - END - ) AS [Details] - FROM [sys].[dm_os_windows_info] [owi]; - - END; - END; - END; - -/* -This check hits the dm_os_process_memory system view -to see if locked_page_allocations_kb is > 0, -which could indicate that locked pages in memory is enabled. -*/ -IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 166 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Locked Pages In Memory Enabled' AS [Finding] , - 'https://BrentOzar.com/go/lpim' AS [URL] , - ( 'You currently have ' - + CASE WHEN [dopm].[locked_page_allocations_kb] / 1024. / 1024. > 0 - THEN CAST([dopm].[locked_page_allocations_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([dopm].[locked_page_allocations_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + ' of pages locked in memory.' ) AS [Details] - FROM - [sys].[dm_os_process_memory] AS [dopm] - WHERE - [dopm].[locked_page_allocations_kb] > 0; - END; - - /* Server Info - Locked Pages In Memory Enabled - Check 166 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'sql_memory_model' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 166 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Memory Model Unconventional'' AS Finding , - ''https://BrentOzar.com/go/lpim'' AS URL , - ''Memory Model: '' + CAST(sql_memory_model_desc AS NVARCHAR(100)) - FROM sys.dm_os_sys_info WHERE sql_memory_model <> 1 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.'; - - EXECUTE(@StringToExecute); - END; - - - - /* - Starting with SQL Server 2014 SP2, Instant File Initialization - is logged in the SQL Server Error Log. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND (@ProductVersionMajor >= 13) OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - INSERT INTO #ErrorLog - EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; - - IF @@ROWCOUNT > 0 - INSERT INTO #BlitzResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 193 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Instant File Initialization Enabled' AS [Finding] , - 'https://BrentOzar.com/go/instant' AS [URL] , - 'The service account has the Perform Volume Maintenance Tasks permission.'; - END; - - /* Server Info - Instant File Initialization Not Enabled - Check 192 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 192 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_server_services' - AND c.name = 'instant_file_initialization_enabled' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 192) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 192 AS CheckID , - 50 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Instant File Initialization Not Enabled'' AS Finding , - ''https://BrentOzar.com/go/instant'' AS URL , - ''Consider enabling IFI for faster restores and data file growths.'' - FROM sys.dm_server_services WHERE instant_file_initialization_enabled <> ''Y'' AND filename LIKE ''%sqlservr.exe%'' 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.'; - - EXECUTE(@StringToExecute); - END; - - - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 130 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 130) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 130 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Name' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - @@SERVERNAME AS Details - WHERE @@SERVERNAME IS NOT NULL; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 83 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 83) WITH NOWAIT; - - -- DATETIMEOFFSET and DATETIME have different minimum values, so there's - -- a small workaround here to force 1753-01-01 if the minimum is detected - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 83 AS CheckID , - 250 AS Priority , - ''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 + ''.'' - FROM sys.dm_server_services 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* Check 84 - SQL Server 2012 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 84 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_kb' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_kb / 1024.0 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - - /* Check 84 - SQL Server 2008 */ - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_in_bytes' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_in_bytes / 1024.0 / 1024 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 85 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 85) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 85 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Service' AS Finding , - '' AS URL , - N'Version: ' - + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) - + N'. Patch Level: ' - + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) - + N'. Edition: ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + N'. AlwaysOn Enabled: ' - + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), - 0) AS VARCHAR(100)) - + N'. AlwaysOn Mgr Status: ' - + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), - 0) AS VARCHAR(100)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 88 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 88) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 88 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Last Restart' AS Finding , - '' AS URL , - CAST(create_date AS VARCHAR(100)) - FROM sys.databases - WHERE database_id = 2; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 91 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 91) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 91 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Last Restart' AS Finding , - '' AS URL , - CAST(DATEADD(SECOND, (ms_ticks/1000)*(-1), GETDATE()) AS nvarchar(25)) - FROM sys.dm_os_sys_info; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 92 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 92) WITH NOWAIT; - - INSERT INTO #driveInfo - ( drive, SIZE ) - EXEC master..xp_fixeddrives; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 92 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Drive ' + i.drive + ' Space' AS Finding , - '' AS URL , - CAST(i.SIZE AS VARCHAR(30)) - + 'MB free on ' + i.drive - + ' drive' AS Details - FROM #driveInfo AS i; - DROP TABLE #driveInfo; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 103 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'virtual_machine_type_desc' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 103) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 103 AS CheckID, - 250 AS Priority, - ''Server Info'' AS FindingsGroup, - ''Virtual Server'' AS Finding, - ''https://BrentOzar.com/go/virtual'' AS URL, - ''Type: ('' + virtual_machine_type_desc + '')'' AS Details - FROM sys.dm_os_sys_info - WHERE virtual_machine_type <> 0 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 114 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_os_memory_nodes' ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_nodes' - AND c.name = 'processor_group' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 114) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 114 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware - NUMA Config'' AS Finding , - '''' AS URL , - ''Node: '' + CAST(n.node_id AS NVARCHAR(10)) + '' State: '' + node_state_desc - + '' Online schedulers: '' + CAST(n.online_scheduler_count AS NVARCHAR(10)) + '' Offline schedulers: '' + CAST(oac.offline_schedulers AS VARCHAR(100)) + '' Processor Group: '' + CAST(n.processor_group AS NVARCHAR(10)) - + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - OUTER APPLY (SELECT - COUNT(*) AS [offline_schedulers] - FROM sys.dm_os_schedulers dos - WHERE n.node_id = dos.parent_node_id - AND dos.status = ''VISIBLE OFFLINE'' - ) oac - WHERE n.node_state_desc NOT LIKE ''%DAC%'' - ORDER BY n.node_id 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 106) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 106 AS CheckID - ,250 AS Priority - ,'Server Info' AS FindingsGroup - ,'Default Trace Contents' AS Finding - ,'https://BrentOzar.com/go/trace' AS URL - ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as VARCHAR(30))+' hours of data' - +' between '+cast(Min(StartTime) as VARCHAR(30))+' and '+cast(GETDATE()as VARCHAR(30)) - +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) - ) as Details - FROM ::fn_trace_gettable( @base_tracefilename, default ) - WHERE EventClass BETWEEN 65500 and 65600; - END; /* CheckID 106 */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 152 ) - BEGIN - IF EXISTS (SELECT * FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 - AND i.wait_type IS NULL) - BEGIN - /* Check for waits that have had more than 10% of the server's wait time */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 152) WITH NOWAIT; - - WITH os(wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) - AS - (SELECT ws.wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms - FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE i.wait_type IS NULL - AND wait_time_ms > .1 * @CpuMsSinceWaitsCleared - AND waiting_tasks_count > 0) - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 9 - 152 AS CheckID - ,240 AS Priority - ,'Wait Stats' AS FindingsGroup - , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding - ,'https://BrentOzar.com/go/waits' AS URL - , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' hours of waits, ' + - CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + - /* CAST(CAST( - 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER () ) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% of waits, ' + */ - CAST(CAST( - 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER ()) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + - CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + - CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 - THEN - CAST( - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) - AS NUMERIC(18,1)) - ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' - FROM os - ORDER BY SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) DESC; - END; /* IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND waiting_tasks_count > 0) */ - - /* If no waits were found, add a note about that */ - IF NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (107, 108, 109, 121, 152, 162)) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 153) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://BrentOzar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); - END; - END; /* CheckID 152 */ - - END; /* IF @CheckServerInfo = 1 */ - END; /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ - - - /* Delete priorites they wanted to skip. */ - IF @IgnorePrioritiesAbove IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] > @IgnorePrioritiesAbove AND CheckID <> -1; - - IF @IgnorePrioritiesBelow IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] < @IgnorePrioritiesBelow AND CheckID <> -1; - - /* Delete checks they wanted to skip. */ - IF @SkipChecksTable IS NOT NULL - BEGIN - DELETE FROM #BlitzResults - WHERE DatabaseName IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE FROM #BlitzResults - WHERE CheckID IN ( SELECT CheckID - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE r FROM #BlitzResults r - INNER JOIN #SkipChecks c ON r.DatabaseName = c.DatabaseName and r.CheckID = c.CheckID - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName')); - END; - - /* Add summary mode */ - IF @SummaryMode > 0 - BEGIN - UPDATE #BlitzResults - SET Finding = br.Finding + ' (' + CAST(brTotals.recs AS NVARCHAR(20)) + ')' - FROM #BlitzResults br - INNER JOIN (SELECT FindingsGroup, Finding, Priority, COUNT(*) AS recs FROM #BlitzResults GROUP BY FindingsGroup, Finding, Priority) brTotals ON br.FindingsGroup = brTotals.FindingsGroup AND br.Finding = brTotals.Finding AND br.Priority = brTotals.Priority - WHERE brTotals.recs > 1; - - DELETE br - FROM #BlitzResults br - WHERE EXISTS (SELECT * FROM #BlitzResults brLower WHERE br.FindingsGroup = brLower.FindingsGroup AND br.Finding = brLower.Finding AND br.Priority = brLower.Priority AND br.ID > brLower.ID); - - END; - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org' , - 'We hope you found this tool useful.' - ); - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - VALUES ( -1 , - 0 , - 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - 'SQL Server First Responder Kit' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - - ); - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - SELECT 156 , - 254 , - 'Rundate' , - GETDATE() , - 'http://FirstResponderKit.org/' , - 'Captain''s log: stardate something and something...'; - - IF @EmailRecipients IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Sending an email.', 0, 1) WITH NOWAIT; - - /* Database mail won't work off a local temp table. I'm not happy about this hacky workaround either. */ - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - SELECT * INTO ##BlitzResults FROM #BlitzResults; - SET @query_result_separator = char(9); - SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup, Finding, Details; SET NOCOUNT OFF;'; - SET @EmailSubject = 'sp_Blitz Results for ' + @@SERVERNAME; - SET @EmailBody = 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. http://FirstResponderKit.org'; - IF @EmailProfile IS NULL - EXEC msdb.dbo.sp_send_dbmail - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - ELSE - EXEC msdb.dbo.sp_send_dbmail - @profile_name = @EmailProfile, - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - END; - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT; - DECLARE @ValidOutputLocation BIT; - DECLARE @LinkedServerDBCheck NVARCHAR(2000); - DECLARE @ValidLinkedServerDB INT; - DECLARE @tmpdbchk table (cnt int); - IF @OutputServerName IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; - - IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) - BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); - IF (@ValidLinkedServerDB > 0) - BEGIN - SET @ValidOutputServer = 1; - SET @ValidOutputLocation = 1; - END; - ELSE - RAISERROR('The specified database was not found on the output server', 16, 0); - END; - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0); - END; - END; - ELSE - BEGIN - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1; - END; - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0); - END; - ELSE - BEGIN - SET @ValidOutputLocation = 0; - END; - END; - - /* @OutputTableName lets us export the results to a permanent table */ - IF @ValidOutputLocation = 1 - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));'; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); - EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END; - ELSE - BEGIN - EXEC(@StringToExecute); - END; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputServerName + '.' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputServerName + '.' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - IF @ValidOutputServer = 1 - BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' - + 'CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); - END; - - - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; - - IF @OutputType = 'COUNT' - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzResults; - END; - ELSE - IF @OutputType IN ( 'CSV', 'RSV' ) - BEGIN - - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputXMLasNVARCHAR = 1 AND @OutputType <> 'NONE' - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan, - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputType = 'MARKDOWN' - BEGIN - WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * - FROM #BlitzResults - WHERE Priority > 0 AND Priority < 255 AND FindingsGroup IS NOT NULL AND Finding IS NOT NULL - AND FindingsGroup <> 'Security' /* Specifically excluding security checks for public exports */) - SELECT - CASE - WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf - ELSE N'' - END - + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END - ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END - END + @crlf - FROM Results r - LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 - LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 - ORDER BY r.rownum FOR XML PATH(N''); - END; - ELSE IF @OutputType <> 'NONE' - BEGIN - /* --TOURSTOP05-- */ - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - [QueryPlan] , - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - - DROP TABLE #BlitzResults; - - IF @OutputProcedureCache = 1 - AND @CheckProcedureCache = 1 - SELECT TOP 20 - total_worker_time / execution_count AS AvgCPU , - total_worker_time AS TotalCPU , - CAST(ROUND(100.00 * total_worker_time - / ( SELECT SUM(total_worker_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentCPU , - total_elapsed_time / execution_count AS AvgDuration , - total_elapsed_time AS TotalDuration , - CAST(ROUND(100.00 * total_elapsed_time - / ( SELECT SUM(total_elapsed_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - CAST(ROUND(100.00 * total_logical_reads - / ( SELECT SUM(total_logical_reads) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentReads , - execution_count , - CAST(ROUND(100.00 * execution_count - / ( SELECT SUM(execution_count) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentExecutions , - CASE WHEN DATEDIFF(mi, creation_time, - qs.last_execution_time) = 0 THEN 0 - ELSE CAST(( 1.00 * execution_count / DATEDIFF(mi, - creation_time, - qs.last_execution_time) ) AS MONEY) - END AS executions_per_minute , - qs.creation_time AS plan_creation_time , - qs.last_execution_time , - text , - text_filtered , - query_plan , - query_plan_filtered , - sql_handle , - query_hash , - plan_handle , - query_plan_hash - FROM #dm_exec_query_stats qs - ORDER BY CASE UPPER(@CheckProcedureCacheFilter) - WHEN 'CPU' THEN total_worker_time - WHEN 'READS' THEN total_logical_reads - WHEN 'EXECCOUNT' THEN execution_count - WHEN 'DURATION' THEN total_elapsed_time - ELSE total_worker_time - END DESC; - - END; /* ELSE -- IF @OutputType = 'SCHEMA' */ - - SET NOCOUNT OFF; -GO - -/* ---Sample execution call with the most common parameters: -EXEC [dbo].[sp_Blitz] - @CheckUserDatabaseObjects = 1 , - @CheckProcedureCache = 0 , - @OutputType = 'TABLE' , - @OutputProcedureCache = 0 , - @CheckProcedureCacheFilter = NULL, - @CheckServerInfo = 1 -*/ -IF OBJECT_ID('dbo.sp_BlitzBackups') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzBackups AS RETURN 0;'); -GO -ALTER PROCEDURE [dbo].[sp_BlitzBackups] - @Help TINYINT = 0 , - @HoursBack INT = 168, - @MSDBName NVARCHAR(256) = 'msdb', - @AGName NVARCHAR(256) = NULL, - @RestoreSpeedFullMBps INT = NULL, - @RestoreSpeedDiffMBps INT = NULL, - @RestoreSpeedLogMBps INT = NULL, - @Debug TINYINT = 0, - @PushBackupHistoryToListener BIT = 0, - @WriteBackupsToListenerName NVARCHAR(256) = NULL, - @WriteBackupsToDatabaseName NVARCHAR(256) = NULL, - @WriteBackupsLastHours INT = 168, - @VersionDate DATE = NULL OUTPUT -WITH RECOMPILE -AS - BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - - IF @Help = 1 PRINT ' - /* - sp_BlitzBackups from http://FirstResponderKit.org - - This script checks your backups to see how much data you might lose when - this server fails, and how long it might take to recover. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @HoursBack INT = 168 How many hours of history to examine, back from now. - You can check just the last 24 hours of backups, for example. - @MSDBName NVARCHAR(255) You can restore MSDB from different servers and check them - centrally. Also useful if you create a DBA utility database - and merge data from several servers in an AG into one DB. - @RestoreSpeedFullMBps INT By default, we use the backup speed from MSDB to guesstimate - how fast your restores will go. If you have done performance - tuning and testing of your backups (or if they horribly go even - slower in your DR environment, and you want to account for - that), then you can pass in different numbers here. - @RestoreSpeedDiffMBps INT See above. - @RestoreSpeedLogMBps INT See above. - - For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - - - - */'; -ELSE -BEGIN -DECLARE @StringToExecute NVARCHAR(MAX) = N'', - @InnerStringToExecute NVARCHAR(MAX) = N'', - @ProductVersion NVARCHAR(128), - @ProductVersionMajor DECIMAL(10, 2), - @ProductVersionMinor DECIMAL(10, 2), - @StartTime DATETIME2, @ResultText NVARCHAR(MAX), - @crlf NVARCHAR(2), - @MoreInfoHeader NVARCHAR(100), - @MoreInfoFooter NVARCHAR(100); - -IF @HoursBack > 0 - SET @HoursBack = @HoursBack * -1; - -IF @WriteBackupsLastHours > 0 - SET @WriteBackupsLastHours = @WriteBackupsLastHours * -1; - -SELECT @crlf = NCHAR(13) + NCHAR(10), - @StartTime = DATEADD(hh, @HoursBack, GETDATE()), - @MoreInfoHeader = N''; - -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); - -CREATE TABLE #Backups -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - RPOWorstCaseMinutes DECIMAL(18, 1), - RTOWorstCaseMinutes DECIMAL(18, 1), - RPOWorstCaseBackupSetID INT, - RPOWorstCaseBackupSetFinishTime DATETIME, - RPOWorstCaseBackupSetIDPrior INT, - RPOWorstCaseBackupSetPriorFinishTime DATETIME, - RPOWorstCaseMoreInfoQuery XML, - RTOWorstCaseBackupFileSizeMB DECIMAL(18, 2), - RTOWorstCaseMoreInfoQuery XML, - FullMBpsAvg DECIMAL(18, 2), - FullMBpsMin DECIMAL(18, 2), - FullMBpsMax DECIMAL(18, 2), - FullSizeMBAvg DECIMAL(18, 2), - FullSizeMBMin DECIMAL(18, 2), - FullSizeMBMax DECIMAL(18, 2), - FullCompressedSizeMBAvg DECIMAL(18, 2), - FullCompressedSizeMBMin DECIMAL(18, 2), - FullCompressedSizeMBMax DECIMAL(18, 2), - DiffMBpsAvg DECIMAL(18, 2), - DiffMBpsMin DECIMAL(18, 2), - DiffMBpsMax DECIMAL(18, 2), - DiffSizeMBAvg DECIMAL(18, 2), - DiffSizeMBMin DECIMAL(18, 2), - DiffSizeMBMax DECIMAL(18, 2), - DiffCompressedSizeMBAvg DECIMAL(18, 2), - DiffCompressedSizeMBMin DECIMAL(18, 2), - DiffCompressedSizeMBMax DECIMAL(18, 2), - LogMBpsAvg DECIMAL(18, 2), - LogMBpsMin DECIMAL(18, 2), - LogMBpsMax DECIMAL(18, 2), - LogSizeMBAvg DECIMAL(18, 2), - LogSizeMBMin DECIMAL(18, 2), - LogSizeMBMax DECIMAL(18, 2), - LogCompressedSizeMBAvg DECIMAL(18, 2), - LogCompressedSizeMBMin DECIMAL(18, 2), - LogCompressedSizeMBMax DECIMAL(18, 2) -); - -CREATE TABLE #RTORecoveryPoints -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - rto_worst_case_size_mb AS - ( COALESCE(log_file_size_mb, 0) + COALESCE(diff_file_size_mb, 0) + COALESCE(full_file_size_mb, 0)), - rto_worst_case_time_seconds AS - ( COALESCE(log_time_seconds, 0) + COALESCE(diff_time_seconds, 0) + COALESCE(full_time_seconds, 0)), - full_backup_set_id INT, - full_last_lsn NUMERIC(25, 0), - full_backup_set_uuid UNIQUEIDENTIFIER, - full_time_seconds BIGINT, - full_file_size_mb DECIMAL(18, 2), - diff_backup_set_id INT, - diff_last_lsn NUMERIC(25, 0), - diff_time_seconds BIGINT, - diff_file_size_mb DECIMAL(18, 2), - log_backup_set_id INT, - log_last_lsn NUMERIC(25, 0), - log_time_seconds BIGINT, - log_file_size_mb DECIMAL(18, 2), - log_backups INT -); - -CREATE TABLE #Recoverability - ( - Id INT IDENTITY , - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - LastBackupRecoveryModel NVARCHAR(60), - FirstFullBackupSizeMB DECIMAL (18,2), - FirstFullBackupDate DATETIME, - LastFullBackupSizeMB DECIMAL (18,2), - LastFullBackupDate DATETIME, - AvgFullBackupThroughputMB DECIMAL (18,2), - AvgFullBackupDurationSeconds INT, - AvgDiffBackupThroughputMB DECIMAL (18,2), - AvgDiffBackupDurationSeconds INT, - AvgLogBackupThroughputMB DECIMAL (18,2), - AvgLogBackupDurationSeconds INT, - AvgFullSizeMB DECIMAL (18,2), - AvgDiffSizeMB DECIMAL (18,2), - AvgLogSizeMB DECIMAL (18,2), - IsBigDiff AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgDiffSizeMB * 100.) / AvgFullSizeMB >= 40.)) THEN 1 ELSE 0 END, - IsBigLog AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgLogSizeMB * 100.) / AvgFullSizeMB >= 20.)) THEN 1 ELSE 0 END - ); - -CREATE TABLE #Trending -( - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - [0] DECIMAL(18, 2), - [-1] DECIMAL(18, 2), - [-2] DECIMAL(18, 2), - [-3] DECIMAL(18, 2), - [-4] DECIMAL(18, 2), - [-5] DECIMAL(18, 2), - [-6] DECIMAL(18, 2), - [-7] DECIMAL(18, 2), - [-8] DECIMAL(18, 2), - [-9] DECIMAL(18, 2), - [-10] DECIMAL(18, 2), - [-11] DECIMAL(18, 2), - [-12] DECIMAL(18, 2) -); - - -CREATE TABLE #Warnings -( - Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckId INT, - Priority INT, - DatabaseName VARCHAR(128), - Finding VARCHAR(256), - Warning VARCHAR(8000) -); - -IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = @MSDBName) - BEGIN - RAISERROR('@MSDBName was specified, but the database does not exist.', 0, 1) WITH NOWAIT; - RETURN; - END - -IF @PushBackupHistoryToListener = 1 -GOTO PushBackupHistoryToListener - - - RAISERROR('Inserting to #Backups', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'WITH Backups AS (SELECT bs.database_name, bs.database_guid, bs.type AS backup_type ' + @crlf - + ' , MBpsAvg = CAST(AVG(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMin = CAST(MIN(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMax = CAST(MAX(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , SizeMBAvg = AVG(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMin = MIN(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMax = MAX(backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBAvg = AVG(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMin = MIN(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMax = MAX(compressed_backup_size / 1048576.0) ' + @crlf; - - - SET @StringToExecute += N' FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bs ' + @crlf - + N' WHERE bs.backup_finish_date >= @StartTime AND bs.is_damaged = 0 ' + @crlf - + N' GROUP BY bs.database_name, bs.database_guid, bs.type)' + @crlf; - - SET @StringToExecute += + N'INSERT INTO #Backups(database_name, database_guid, ' + @crlf - + N' FullMBpsAvg, FullMBpsMin, FullMBpsMax, FullSizeMBAvg, FullSizeMBMin, FullSizeMBMax, FullCompressedSizeMBAvg, FullCompressedSizeMBMin, FullCompressedSizeMBMax, ' + @crlf - + N' DiffMBpsAvg, DiffMBpsMin, DiffMBpsMax, DiffSizeMBAvg, DiffSizeMBMin, DiffSizeMBMax, DiffCompressedSizeMBAvg, DiffCompressedSizeMBMin, DiffCompressedSizeMBMax, ' + @crlf - + N' LogMBpsAvg, LogMBpsMin, LogMBpsMax, LogSizeMBAvg, LogSizeMBMin, LogSizeMBMax, LogCompressedSizeMBAvg, LogCompressedSizeMBMin, LogCompressedSizeMBMax ) ' + @crlf - + N'SELECT bF.database_name, bF.database_guid ' + @crlf - + N' , bF.MBpsAvg AS FullMBpsAvg ' + @crlf - + N' , bF.MBpsMin AS FullMBpsMin ' + @crlf - + N' , bF.MBpsMax AS FullMBpsMax ' + @crlf - + N' , bF.SizeMBAvg AS FullSizeMBAvg ' + @crlf - + N' , bF.SizeMBMin AS FullSizeMBMin ' + @crlf - + N' , bF.SizeMBMax AS FullSizeMBMax ' + @crlf - + N' , bF.CompressedSizeMBAvg AS FullCompressedSizeMBAvg ' + @crlf - + N' , bF.CompressedSizeMBMin AS FullCompressedSizeMBMin ' + @crlf - + N' , bF.CompressedSizeMBMax AS FullCompressedSizeMBMax ' + @crlf - + N' , bD.MBpsAvg AS DiffMBpsAvg ' + @crlf - + N' , bD.MBpsMin AS DiffMBpsMin ' + @crlf - + N' , bD.MBpsMax AS DiffMBpsMax ' + @crlf - + N' , bD.SizeMBAvg AS DiffSizeMBAvg ' + @crlf - + N' , bD.SizeMBMin AS DiffSizeMBMin ' + @crlf - + N' , bD.SizeMBMax AS DiffSizeMBMax ' + @crlf - + N' , bD.CompressedSizeMBAvg AS DiffCompressedSizeMBAvg ' + @crlf - + N' , bD.CompressedSizeMBMin AS DiffCompressedSizeMBMin ' + @crlf - + N' , bD.CompressedSizeMBMax AS DiffCompressedSizeMBMax ' + @crlf - + N' , bL.MBpsAvg AS LogMBpsAvg ' + @crlf - + N' , bL.MBpsMin AS LogMBpsMin ' + @crlf - + N' , bL.MBpsMax AS LogMBpsMax ' + @crlf - + N' , bL.SizeMBAvg AS LogSizeMBAvg ' + @crlf - + N' , bL.SizeMBMin AS LogSizeMBMin ' + @crlf - + N' , bL.SizeMBMax AS LogSizeMBMax ' + @crlf - + N' , bL.CompressedSizeMBAvg AS LogCompressedSizeMBAvg ' + @crlf - + N' , bL.CompressedSizeMBMin AS LogCompressedSizeMBMin ' + @crlf - + N' , bL.CompressedSizeMBMax AS LogCompressedSizeMBMax ' + @crlf - + N' FROM Backups bF ' + @crlf - + N' LEFT OUTER JOIN Backups bD ON bF.database_name = bD.database_name AND bF.database_guid = bD.database_guid AND bD.backup_type = ''I''' + @crlf - + N' LEFT OUTER JOIN Backups bL ON bF.database_name = bL.database_name AND bF.database_guid = bL.database_guid AND bL.backup_type = ''L''' + @crlf - + N' WHERE bF.backup_type = ''D''; ' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - RAISERROR('Updating #Backups with worst RPO case', 0, 1) WITH NOWAIT; - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - SELECT bs.database_name, bs.database_guid, bs.backup_set_id, bsPrior.backup_set_id AS backup_set_id_prior, - bs.backup_finish_date, bsPrior.backup_finish_date AS backup_finish_date_prior, - DATEDIFF(ss, bsPrior.backup_finish_date, bs.backup_finish_date) AS backup_gap_seconds - INTO #backup_gaps - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs - CROSS APPLY ( - SELECT TOP 1 bs1.backup_set_id, bs1.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs1 - WHERE bs.database_name = bs1.database_name - AND bs.database_guid = bs1.database_guid - AND bs.backup_finish_date > bs1.backup_finish_date - AND bs.backup_set_id > bs1.backup_set_id - ORDER BY bs1.backup_finish_date DESC, bs1.backup_set_id DESC - ) bsPrior - WHERE bs.backup_finish_date > @StartTime - - CREATE CLUSTERED INDEX cx_backup_gaps ON #backup_gaps (database_name, database_guid, backup_set_id, backup_finish_date, backup_gap_seconds); - - WITH max_gaps AS ( - SELECT g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, - g.backup_finish_date, MAX(g.backup_gap_seconds) AS max_backup_gap_seconds - FROM #backup_gaps AS g - GROUP BY g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, g.backup_finish_date - ) - UPDATE #Backups - SET RPOWorstCaseMinutes = bg.max_backup_gap_seconds / 60.0 - , RPOWorstCaseBackupSetID = bg.backup_set_id - , RPOWorstCaseBackupSetFinishTime = bg.backup_finish_date - , RPOWorstCaseBackupSetIDPrior = bg.backup_set_id_prior - , RPOWorstCaseBackupSetPriorFinishTime = bg.backup_finish_date_prior - FROM #Backups b - INNER HASH JOIN max_gaps bg ON b.database_name = bg.database_name AND b.database_guid = bg.database_guid - LEFT OUTER HASH JOIN max_gaps bgBigger ON bg.database_name = bgBigger.database_name AND bg.database_guid = bgBigger.database_guid AND bg.max_backup_gap_seconds < bgBigger.max_backup_gap_seconds - WHERE bgBigger.backup_set_id IS NULL; - '; - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - RAISERROR('Updating #Backups with worst RPO case queries', 0, 1) WITH NOWAIT; - - UPDATE #Backups - SET RPOWorstCaseMoreInfoQuery = @MoreInfoHeader + N'SELECT * ' + @crlf - + N' FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset ' + @crlf - + N' WHERE database_name = ''' + database_name + ''' ' + @crlf - + N' AND database_guid = ''' + CAST(database_guid AS NVARCHAR(50)) + ''' ' + @crlf - + N' AND backup_finish_date >= DATEADD(hh, -2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' AND backup_finish_date <= DATEADD(hh, 2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' ORDER BY backup_finish_date;' - + @MoreInfoFooter; - - -/* RTO */ - -RAISERROR('Gathering RTO information', 0, 1) WITH NOWAIT; - - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, log_last_lsn) - SELECT database_name, database_guid, MAX(last_lsn) AS log_last_lsn - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset bLastLog - WHERE type = ''L'' - AND bLastLog.backup_finish_date >= @StartTime - GROUP BY database_name, database_guid; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/* Find the most recent full backups for those logs */ - -RAISERROR('Updating #RTORecoveryPoints', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET log_backup_set_id = bLasted.backup_set_id - ,full_backup_set_id = bLasted.backup_set_id - ,full_last_lsn = bLasted.last_lsn - ,full_backup_set_uuid = bLasted.backup_set_uuid - FROM #RTORecoveryPoints rp - CROSS APPLY ( - SELECT TOP 1 bLog.backup_set_id AS backup_set_id_log, bLastFull.backup_set_id, bLastFull.last_lsn, bLastFull.backup_set_uuid, bLastFull.database_guid, bLastFull.database_name - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLastFull - ON bLog.database_guid = bLastFull.database_guid - AND bLog.database_name = bLastFull.database_name - AND bLog.first_lsn > bLastFull.last_lsn - AND bLastFull.type = ''D'' - WHERE rp.database_guid = bLog.database_guid - AND rp.database_name = bLog.database_name - ) bLasted - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLaterFulls ON bLasted.database_guid = bLaterFulls.database_guid AND bLasted.database_name = bLaterFulls.database_name - AND bLasted.last_lsn < bLaterFulls.last_lsn - AND bLaterFulls.first_lsn < bLasted.last_lsn - AND bLaterFulls.type = ''D'' - WHERE bLaterFulls.backup_set_id IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Add any full backups in the StartDate range that weren't part of the above log backup chain */ - -RAISERROR('Add any full backups in the StartDate range that weren''t part of the above log backup chain', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, full_backup_set_id, full_last_lsn, full_backup_set_uuid) - SELECT bFull.database_name, bFull.database_guid, bFull.backup_set_id, bFull.last_lsn, bFull.backup_set_uuid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull - LEFT OUTER JOIN #RTORecoveryPoints rp ON bFull.backup_set_uuid = rp.full_backup_set_uuid - WHERE bFull.type = ''D'' - AND bFull.backup_finish_date IS NOT NULL - AND rp.full_backup_set_uuid IS NULL - AND bFull.backup_finish_date >= @StartTime; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/* Fill out the most recent log for that full, but before the next full */ - -RAISERROR('Fill out the most recent log for that full, but before the next full', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE rp - SET log_last_lsn = (SELECT MAX(last_lsn) FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog WHERE bLog.first_lsn >= rp.full_last_lsn AND bLog.first_lsn <= rpNextFull.full_last_lsn AND bLog.type = ''L'') - FROM #RTORecoveryPoints rp - INNER JOIN #RTORecoveryPoints rpNextFull ON rp.database_guid = rpNextFull.database_guid AND rp.database_name = rpNextFull.database_name - AND rp.full_last_lsn < rpNextFull.full_last_lsn - LEFT OUTER JOIN #RTORecoveryPoints rpEarlierFull ON rp.database_guid = rpEarlierFull.database_guid AND rp.database_name = rpEarlierFull.database_name - AND rp.full_last_lsn < rpEarlierFull.full_last_lsn - AND rpNextFull.full_last_lsn > rpEarlierFull.full_last_lsn - WHERE rpEarlierFull.full_backup_set_id IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Fill out a diff in that range */ - -RAISERROR('Fill out a diff in that range', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET diff_last_lsn = (SELECT TOP 1 bDiff.last_lsn FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff - WHERE rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name - AND bDiff.type = ''I'' - AND bDiff.last_lsn < rp.log_last_lsn - AND rp.full_backup_set_uuid = bDiff.differential_base_guid - ORDER BY bDiff.last_lsn DESC) - FROM #RTORecoveryPoints rp - WHERE diff_last_lsn IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Get time & size totals for full & diff */ - -RAISERROR('Get time & size totals for full & diff', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET full_time_seconds = DATEDIFF(ss,bFull.backup_start_date, bFull.backup_finish_date) - , full_file_size_mb = bFull.backup_size / 1048576.0 - , diff_backup_set_id = bDiff.backup_set_id - , diff_time_seconds = DATEDIFF(ss,bDiff.backup_start_date, bDiff.backup_finish_date) - , diff_file_size_mb = bDiff.backup_size / 1048576.0 - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull ON rp.database_guid = bFull.database_guid AND rp.database_name = bFull.database_name AND rp.full_last_lsn = bFull.last_lsn - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff ON rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name AND rp.diff_last_lsn = bDiff.last_lsn AND bDiff.last_lsn IS NOT NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - - -/* Get time & size totals for logs */ - -RAISERROR('Get time & size totals for logs', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH LogTotals AS ( - SELECT rp.id, log_time_seconds = SUM(DATEDIFF(ss,bLog.backup_start_date, bLog.backup_finish_date)) - , log_file_size = SUM(bLog.backup_size) - , SUM(1) AS log_backups - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog ON rp.database_guid = bLog.database_guid AND rp.database_name = bLog.database_name AND bLog.type = ''L'' - AND bLog.first_lsn > COALESCE(rp.diff_last_lsn, rp.full_last_lsn) - AND bLog.first_lsn <= rp.log_last_lsn - GROUP BY rp.id - ) - UPDATE #RTORecoveryPoints - SET log_time_seconds = lt.log_time_seconds - , log_file_size_mb = lt.log_file_size / 1048576.0 - , log_backups = lt.log_backups - FROM #RTORecoveryPoints rp - INNER JOIN LogTotals lt ON rp.id = lt.id; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -RAISERROR('Gathering RTO worst cases', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH WorstCases AS ( - SELECT rp.* - FROM #RTORecoveryPoints rp - LEFT OUTER JOIN #RTORecoveryPoints rpNewer - ON rp.database_guid = rpNewer.database_guid - AND rp.database_name = rpNewer.database_name - AND rp.full_last_lsn < rpNewer.full_last_lsn - AND rpNewer.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - WHERE rp.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - /* OR rp.rto_worst_case_time_seconds = (SELECT TOP 1 rto_worst_case_time_seconds FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_time_seconds DESC) */ - AND rpNewer.database_guid IS NULL - ) - UPDATE #Backups - SET RTOWorstCaseMinutes = - /* Fulls */ - (CASE WHEN @RestoreSpeedFullMBps IS NULL - THEN wc.full_time_seconds / 60.0 - ELSE @RestoreSpeedFullMBps / wc.full_file_size_mb - END) - - /* Diffs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedDiffMBps IS NOT NULL AND wc.diff_file_size_mb IS NOT NULL - THEN @RestoreSpeedDiffMBps / wc.diff_file_size_mb - ELSE COALESCE(wc.diff_time_seconds,0) / 60.0 - END) - - /* Logs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedLogMBps IS NOT NULL AND wc.log_file_size_mb IS NOT NULL - THEN @RestoreSpeedLogMBps / wc.log_file_size_mb - ELSE COALESCE(wc.log_time_seconds,0) / 60.0 - END) - , RTOWorstCaseBackupFileSizeMB = wc.rto_worst_case_size_mb - FROM #Backups b - INNER JOIN WorstCases wc - ON b.database_guid = wc.database_guid - AND b.database_name = wc.database_name; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@RestoreSpeedFullMBps INT, @RestoreSpeedDiffMBps INT, @RestoreSpeedLogMBps INT', @RestoreSpeedFullMBps, @RestoreSpeedDiffMBps, @RestoreSpeedLogMBps; - - - -/*Populating Recoverability*/ - - - /*Get distinct list of databases*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - SELECT DISTINCT b.database_name, database_guid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b;' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Recoverability ( DatabaseName, DatabaseGUID ) - EXEC sys.sp_executesql @StringToExecute; - - - /*Find most recent recovery model, backup size, and backup date*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.LastBackupRecoveryModel = ca.recovery_model, - r.LastFullBackupSizeMB = ca.compressed_backup_size, - r.LastFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 b.recovery_model, (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date DESC - ) ca;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - /*Find first backup size and date*/ - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.FirstFullBackupSizeMB = ca.compressed_backup_size, - r.FirstFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date ASC - ) ca;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - /*Find average backup throughputs for full, diff, and log*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullBackupThroughputMB = ca_full.AvgFullSpeed, - r.AvgDiffBackupThroughputMB = ca_diff.AvgDiffSpeed, - r.AvgLogBackupThroughputMB = ca_log.AvgLogSpeed, - r.AvgFullBackupDurationSeconds = AvgFullDuration, - r.AvgDiffBackupDurationSeconds = AvgDiffDuration, - r.AvgLogBackupDurationSeconds = AvgLogDuration - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgFullSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgFullDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_full - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgDiffSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgDiffDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_diff - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgLogSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgLogDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_log;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - /*Find max and avg diff and log sizes*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullSizeMB = fulls.avg_full_size, - r.AvgDiffSizeMB = diffs.avg_diff_size, - r.AvgLogSizeMB = logs.avg_log_size - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_full_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS fulls - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_diff_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS diffs - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_log_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS logs;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/*Trending - only works if backupfile is populated, which means in msdb */ -IF @MSDBName = N'msdb' -BEGIN - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' --+ @crlf; - - SET @StringToExecute += N' - SELECT p.DatabaseName, - p.DatabaseGUID, - p.[0], - p.[-1], - p.[-2], - p.[-3], - p.[-4], - p.[-5], - p.[-6], - p.[-7], - p.[-8], - p.[-9], - p.[-10], - p.[-11], - p.[-12] - FROM ( SELECT b.database_name AS DatabaseName, - b.database_guid AS DatabaseGUID, - DATEDIFF(MONTH, @StartTime, b.backup_start_date) AS MonthsAgo , - CONVERT(DECIMAL(18, 2), AVG(bf.file_size / 1048576.0)) AS AvgSizeMB - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupfile AS bf - ON b.backup_set_id = bf.backup_set_id - WHERE b.database_name NOT IN ( ''master'', ''msdb'', ''model'', ''tempdb'' ) - AND bf.file_type = ''D'' - AND b.backup_start_date >= DATEADD(YEAR, -1, @StartTime) - AND b.backup_start_date <= SYSDATETIME() - GROUP BY b.database_name, - b.database_guid, - DATEDIFF(mm, @StartTime, b.backup_start_date) - ) AS bckstat PIVOT ( SUM(bckstat.AvgSizeMB) FOR bckstat.MonthsAgo IN ( [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) ) AS p - ORDER BY p.DatabaseName; - ' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Trending ( DatabaseName, DatabaseGUID, [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -END - -/*End Trending*/ - -/*End populating Recoverability*/ - -RAISERROR('Returning data', 0, 1) WITH NOWAIT; - - SELECT b.* - FROM #Backups AS b - ORDER BY b.database_name; - - SELECT r.*, - t.[0], t.[-1], t.[-2], t.[-3], t.[-4], t.[-5], t.[-6], t.[-7], t.[-8], t.[-9], t.[-10], t.[-11], t.[-12] - FROM #Recoverability AS r - LEFT JOIN #Trending t - ON r.DatabaseName = t.DatabaseName - AND r.DatabaseGUID = t.DatabaseGUID - WHERE r.LastBackupRecoveryModel IS NOT NULL - ORDER BY r.DatabaseName - - -RAISERROR('Rules analysis starting', 0, 1) WITH NOWAIT; - -/*Looking for out of band backups by finding most common backup operator user_name and noting backups taken by other user_names*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH common_people AS ( - SELECT TOP 1 b.user_name, COUNT_BIG(*) AS Records - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.user_name - ORDER BY Records DESC - ) - SELECT - 1 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Non-Agent backups taken'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up by '' + QUOTENAME(b.user_name) + '' '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.user_name NOT LIKE ''%Agent%'' AND b.user_name NOT LIKE ''%AGENT%'' - AND NOT EXISTS ( - SELECT 1 - FROM common_people AS cp - WHERE cp.user_name = b.user_name - ) - GROUP BY b.database_name, b.user_name - HAVING COUNT(*) > 1;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings (CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for compatibility level changing. Only looking for databases that have changed more than twice (It''s possible someone may have changed up, had CE problems, and then changed back)*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 2 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Compatibility level changing'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed compatibility levels '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.compatibility_level)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.compatibility_level) > 2;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for password protected backups. This hasn''t been a popular option ever, and was largely replaced by encrypted backups, but it''s simple to check for.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 3 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Password backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up with a password '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times. Who has the password?'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_password_protected = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for snapshot backups. There are legit reasons for these, but we should flag them so the questions get asked. What questions? Good question.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 4 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Snapshot backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' snapshot backups. This message is purely informational.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_snapshot = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*It''s fine to take backups of read only databases, but it''s not always necessary (there''s no new data, after all).*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 5 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Read only state backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in a read-only state. This can be normal if it''''s a secondary, but a bit odd otherwise.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_readonly = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*So, I''ve come across people who think they need to change their database to single user mode to take a backup. Or that doing that will help something. I just need to know, here.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 6 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Single user mode backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in single-user mode. This is really weird! Make sure your backup process doesn''''t include a mode change anywhere.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_single_user = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*C''mon, it''s 2017. Take your backups with CHECKSUMS, people.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 7 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''No CHECKSUMS'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times without CHECKSUMS in the past 30 days. CHECKSUMS can help alert you to corruption errors.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.has_backup_checksums = 0 - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Damaged is a Black Flag album. You don''t want your backups to be like a Black Flag album. */ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 8 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Damaged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' damaged backups taken without stopping to throw an error. This is done by specifying CONTINUE_AFTER_ERROR in your BACKUP commands.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_damaged = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Checking for encrypted backups and the last backup of the encryption key.*/ - - /*2014 ONLY*/ - -IF @ProductVersionMajor >= 12 - BEGIN - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 9 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Encrypted backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' '' + b.encryptor_type + '' backups, and the last time a certificate was backed up is ' - + CASE WHEN LOWER(@MSDBName) <> N'msdb' - THEN + N'...well, that information is on another server, anyway.'' AS [Warning]' - ELSE + CONVERT(VARCHAR(30), (SELECT MAX(c.pvt_key_last_backup_date) FROM sys.certificates AS c WHERE c.name NOT LIKE '##%')) + N'.'' AS [Warning]' - END + - N' - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.encryptor_type IS NOT NULL - GROUP BY b.database_name, b.encryptor_type;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - END - - /*Looking for backups that have BULK LOGGED data in them -- this can screw up point in time LOG recovery.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 10 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Bulk logged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' backups with bulk logged data. This can make point in time recovery awkward. '' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.has_bulk_logged_data = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for recovery model being switched between FULL and SIMPLE, because it''s a bad practice.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 11 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Recovery model switched'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed recovery models from between FULL and SIMPLE '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.recovery_model)) + '' times. This breaks the log chain and is generally a bad idea.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.recovery_model <> ''BULK-LOGGED'' - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.recovery_model) > 4;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for uncompressed backups.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 12 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Uncompressed backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' uncompressed backups in the last 30 days. This is a free way to save time and space. And SPACETIME. If your version of SQL supports it.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE backup_size = compressed_backup_size AND type = ''D'' - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - -RAISERROR('Rules analysis starting on temp tables', 0, 1) WITH NOWAIT; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Diffs' AS [Finding], - 'On average, Differential backups for this database are >=40% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigDiff = 1 - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Logs' AS [Finding], - 'On average, Log backups for this database are >=20% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigLog = 1 - - - -/*Insert thank you stuff last*/ - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - - SELECT - 2147483647 AS [CheckId], - 2147483647 AS [Priority], - 'From Your Community Volunteers' AS [DatabaseName], - 'sp_BlitzBackups Version: ' + @Version + ', Version Date: ' + CONVERT(VARCHAR(30), @VersionDate) + '.' AS [Finding], - 'Thanks for using our stored procedure. We hope you find it useful! Check out our other free SQL Server scripts at firstresponderkit.org!' AS [Warning]; - -RAISERROR('Rules analysis finished', 0, 1) WITH NOWAIT; - -SELECT w.CheckId, w.Priority, w.DatabaseName, w.Finding, w.Warning -FROM #Warnings AS w -ORDER BY w.Priority, w.CheckId; - -DROP TABLE #Backups, #Warnings, #Recoverability, #RTORecoveryPoints - - -RETURN; - -PushBackupHistoryToListener: - -RAISERROR('Pushing backup history to listener', 0, 1) WITH NOWAIT; - -DECLARE @msg NVARCHAR(4000) = N''; -DECLARE @RemoteCheck TABLE (c INT NULL); - - -IF @WriteBackupsToDatabaseName IS NULL - BEGIN - RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 0, 1) WITH NOWAIT - RETURN; - END - -IF LOWER(@WriteBackupsToDatabaseName) = N'msdb' - BEGIN - RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 0, 1) WITH NOWAIT - RETURN; - END - -IF @WriteBackupsToListenerName IS NULL -BEGIN - IF @AGName IS NULL - BEGIN - RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 0, 1) WITH NOWAIT; - RETURN; - END - ELSE - BEGIN - SELECT @WriteBackupsToListenerName = dns_name - FROM sys.availability_groups AS ag - JOIN sys.availability_group_listeners AS agl - ON ag.group_id = agl.group_id - WHERE name = @AGName; - END - -END - -IF @WriteBackupsToListenerName IS NOT NULL -BEGIN - IF NOT EXISTS - ( - SELECT * - FROM sys.servers s - WHERE name = @WriteBackupsToListenerName - ) - BEGIN - SET @msg = N'We need a linked server to write data across. Please set one up for ' + @WriteBackupsToListenerName + N'.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - RETURN; - END -END - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + N'.master.sys.databases d WHERE d.name = @i_WriteBackupsToDatabaseName;' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsToDatabaseName NVARCHAR(256)', @i_WriteBackupsToDatabaseName = @WriteBackupsToDatabaseName; - - IF @@ROWCOUNT = 0 - BEGIN - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to exist on that server.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RETURN; - END - - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + '.' + QUOTENAME(@WriteBackupsToDatabaseName) + '.sys.tables WHERE name = ''backupset'' AND SCHEMA_NAME(schema_id) = ''dbo''; - ' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute; - - IF @@ROWCOUNT = 0 - BEGIN - - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to have a table called dbo.backupset in it.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RAISERROR('Don''t worry, we''ll create it for you!', 0, 1) WITH NOWAIT - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'CREATE TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ( backup_set_id INT IDENTITY(1, 1), backup_set_uuid UNIQUEIDENTIFIER, media_set_id INT, first_family_number TINYINT, first_media_number SMALLINT, - last_family_number TINYINT, last_media_number SMALLINT, catalog_family_number TINYINT, catalog_media_number SMALLINT, position INT, expiration_date DATETIME, - software_vendor_id INT, name NVARCHAR(128), description NVARCHAR(255), user_name NVARCHAR(128), software_major_version TINYINT, software_minor_version TINYINT, - software_build_version SMALLINT, time_zone SMALLINT, mtf_minor_version TINYINT, first_lsn NUMERIC(25, 0), last_lsn NUMERIC(25, 0), checkpoint_lsn NUMERIC(25, 0), - database_backup_lsn NUMERIC(25, 0), database_creation_date DATETIME, backup_start_date DATETIME, backup_finish_date DATETIME, type CHAR(1), sort_order SMALLINT, - code_page SMALLINT, compatibility_level TINYINT, database_version INT, backup_size NUMERIC(20, 0), database_name NVARCHAR(128), server_name NVARCHAR(128), - machine_name NVARCHAR(128), flags INT, unicode_locale INT, unicode_compare_style INT, collation_name NVARCHAR(128), is_password_protected BIT, recovery_model NVARCHAR(60), - has_bulk_logged_data BIT, is_snapshot BIT, is_readonly BIT, is_single_user BIT, has_backup_checksums BIT, is_damaged BIT, begins_log_chain BIT, has_incomplete_metadata BIT, - is_force_offline BIT, is_copy_only BIT, first_recovery_fork_guid UNIQUEIDENTIFIER, last_recovery_fork_guid UNIQUEIDENTIFIER, fork_point_lsn NUMERIC(25, 0), database_guid UNIQUEIDENTIFIER, - family_guid UNIQUEIDENTIFIER, differential_base_lsn NUMERIC(25, 0), differential_base_guid UNIQUEIDENTIFIER, compressed_backup_size NUMERIC(20, 0), key_algorithm NVARCHAR(32), - encryptor_thumbprint VARBINARY(20) , encryptor_type NVARCHAR(32) - ); - ' + @crlf; - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - RAISERROR('We''ll even make the indexes!', 0, 1) WITH NOWAIT - - /*Checking for and creating the PK/CX*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - - IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name LIKE ? - ) - - BEGIN - ALTER TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ADD PRIMARY KEY CLUSTERED ([backup_set_id] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''PK[_][_]%'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on backup_set_uuid*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetuuid] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_set_uuid] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetuuid'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on media_set_id*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += 'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetMediaSetId] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([media_set_id] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetMediaSetId'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on backup_finish_date*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDate] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_finish_date] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDate'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on database_name*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDatabaseName] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([database_name] ASC ) INCLUDE ([backup_set_id], [media_set_id]) - END - - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDatabaseName'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - RAISERROR('Table and indexes created! You''re welcome!', 0, 1) WITH NOWAIT - END - - - RAISERROR('Beginning inserts', 0, 1) WITH NOWAIT; - RAISERROR(@crlf, 0, 1) WITH NOWAIT; - - /* - Batching code comes from the lovely and talented Michael J. Swart - http://michaeljswart.com/2014/09/take-care-when-scripting-batches/ - If you're ever in Canada, he says you can stay at his house, too. - */ - - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - DECLARE - @StartDate DATETIME = DATEADD(HOUR, @i_WriteBackupsLastHours, SYSDATETIME()), - @StartDateNext DATETIME, - @RC INT = 1, - @msg NVARCHAR(4000) = N''''; - - SELECT @StartDate = MIN(b.backup_start_date) - FROM msdb.dbo.backupset b - WHERE b.backup_start_date >= @StartDate - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) - - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - - IF - ( @StartDate IS NULL ) - BEGIN - SET @msg = N''No data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - RETURN; - END - - RAISERROR(''Starting insert loop'', 0, 1) WITH NOWAIT; - - WHILE EXISTS ( - SELECT 1 - FROM msdb.dbo.backupset b - WHERE NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) - ) - BEGIN - - SET @msg = N''Inserting data for '' + CONVERT(NVARCHAR(30), @StartDate) + '' through '' + + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - ' - - SET @StringToExecute += N'INSERT ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ' - SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data)' + @crlf - ELSE + N'has_bulk_logged_data)' + @crlf - END - - SET @StringToExecute +=N' - SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data' + @crlf - ELSE + N'has_bulk_logged_data' + @crlf - END - SET @StringToExecute +=N' - FROM msdb.dbo.backupset b - WHERE 1=1 - AND b.backup_start_date >= @StartDate - AND b.backup_start_date < @StartDateNext - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - )' + @crlf; - - - SET @StringToExecute +=N' - SET @RC = @@ROWCOUNT; - - SET @msg = N''Inserted '' + CONVERT(NVARCHAR(30), @RC) + '' rows for ''+ CONVERT(NVARCHAR(30), @StartDate) + '' through '' + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - SET @StartDate = @StartDateNext; - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - - IF - ( @StartDate > SYSDATETIME() ) - BEGIN - - SET @msg = N''No more data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - BREAK; - - END - END' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsLastHours INT', @i_WriteBackupsLastHours = @WriteBackupsLastHours; - -END; - -END; - -GO -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @msg VARCHAR(8000); - SELECT @msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; -END; - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); -GO - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheProcs;'); -GO - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheResults;'); -GO - -CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) -); - -CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - /*The Memory Grant columns are only supported - in certain versions, giggle giggle. - */ - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); -GO - -ALTER PROCEDURE dbo.sp_BlitzCache - @Help BIT = 0, - @Top INT = NULL, - @SortOrder VARCHAR(50) = 'CPU', - @UseTriggersAnyway BIT = NULL, - @ExportToExcel BIT = 0, - @ExpertMode TINYINT = 0, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @ConfigurationDatabaseName NVARCHAR(128) = NULL , - @ConfigurationSchemaName NVARCHAR(256) = NULL , - @ConfigurationTableName NVARCHAR(256) = NULL , - @DurationFilter DECIMAL(38,4) = NULL , - @HideSummary BIT = 0 , - @IgnoreSystemDBs BIT = 1 , - @OnlyQueryHashes VARCHAR(MAX) = NULL , - @IgnoreQueryHashes VARCHAR(MAX) = NULL , - @OnlySqlHandles VARCHAR(MAX) = NULL , - @IgnoreSqlHandles VARCHAR(MAX) = NULL , - @QueryFilter VARCHAR(10) = 'ALL' , - @DatabaseName NVARCHAR(128) = NULL , - @StoredProcName NVARCHAR(128) = NULL, - @Reanalyze BIT = 0 , - @SkipAnalysis BIT = 0 , - @BringThePain BIT = 0, /* This will forcibly set @Top to 2,147,483,647 */ - @MinimumExecutionCount INT = 0, - @Debug BIT = 0, - @CheckDateOverride DATETIMEOFFSET = NULL, - @MinutesBack INT = NULL, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - -IF @Help = 1 PRINT ' -sp_BlitzCache from http://FirstResponderKit.org - -This script displays your most resource-intensive queries from the plan cache, -and points to ways you can tune these queries to make them faster. - - -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. - -Known limitations of this version: - - This query will not run on SQL Server 2005. - - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is - excluded by default. - - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes - with no spaces between the hash values. - - @OutputServerName is not functional yet. - -Unknown limitations of this version: - - May or may not be vulnerable to the wick effect. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -'; - -DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; - -IF @Help = 1 -BEGIN - SELECT N'@Help' AS [Parameter Name] , - N'BIT' AS [Data Type] , - N'Displays this help message.' AS [Parameter Description] - - UNION ALL - SELECT N'@Top', - N'INT', - N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' - - UNION ALL - SELECT N'@SortOrder', - N'VARCHAR(10)', - N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' - - UNION ALL - SELECT N'@UseTriggersAnyway', - N'BIT', - N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' - - UNION ALL - SELECT N'@ExportToExcel', - N'BIT', - N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' - - UNION ALL - SELECT N'@ExpertMode', - N'TINYINT', - N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' - - UNION ALL - SELECT N'@OutputDatabaseName', - N'NVARCHAR(128)', - N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputSchemaName', - N'NVARCHAR(256)', - N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputTableName', - N'NVARCHAR(256)', - N'The output table. If this does not exist, it will be created for you.' - - UNION ALL - SELECT N'@DurationFilter', - N'DECIMAL(38,4)', - N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' - - UNION ALL - SELECT N'@HideSummary', - N'BIT', - N'Hides the findings summary result set.' - - UNION ALL - SELECT N'@IgnoreSystemDBs', - N'BIT', - N'Ignores plans found in the system databases (master, model, msdb, tempdb, and resourcedb)' - - UNION ALL - SELECT N'@OnlyQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' - - UNION ALL - SELECT N'@IgnoreQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to ignore.' - - UNION ALL - SELECT N'@OnlySqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to use for filtering results.' - - UNION ALL - SELECT N'@IgnoreSqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to ignore.' - - UNION ALL - SELECT N'@DatabaseName', - N'NVARCHAR(128)', - N'A database name which is used for filtering results.' - - UNION ALL - SELECT N'@StoredProcName', - N'NVARCHAR(128)', - N'Name of stored procedure you want to find plans for.' - - UNION ALL - SELECT N'@BringThePain', - N'BIT', - N'This forces sp_BlitzCache to examine the entire plan cache. Be careful running this on servers with a lot of memory or a large execution plan cache.' - - UNION ALL - SELECT N'@QueryFilter', - N'VARCHAR(10)', - N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' - - UNION ALL - SELECT N'@Reanalyze', - N'BIT', - N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' - - UNION ALL - SELECT N'@MinimumExecutionCount', - N'INT', - N'Queries with fewer than this number of executions will be omitted from results.' - - UNION ALL - SELECT N'@Debug', - N'BIT', - N'Setting this to 1 will print dynamic SQL and select data from all tables used.' - - UNION ALL - SELECT N'@MinutesBack', - N'INT', - N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negtive.'; - - - /* Column definitions */ - SELECT N'# Executions' AS [Column Name], - N'BIGINT' AS [Data Type], - N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] - - UNION ALL - SELECT N'Executions / Minute', - N'MONEY', - N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' - - UNION ALL - SELECT N'Execution Weight', - N'MONEY', - N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Database', - N'sysname', - N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' - - UNION ALL - SELECT N'Total CPU', - N'BIGINT', - N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' - - UNION ALL - SELECT N'Avg CPU', - N'BIGINT', - N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'CPU Weight', - N'MONEY', - N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Duration', - N'BIGINT', - N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' - - UNION ALL - SELECT N'Avg Duration', - N'BIGINT', - N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'Duration Weight', - N'MONEY', - N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Reads', - N'BIGINT', - N'Total logical reads performed by this query since last compilation.' - - UNION ALL - SELECT N'Average Reads', - N'BIGINT', - N'Average logical reads performed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'Read Weight', - N'MONEY', - N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Writes', - N'BIGINT', - N'Total logical writes performed by this query since last compilation.' - - UNION ALL - SELECT N'Average Writes', - N'BIGINT', - N'Average logical writes performed by each execution this query since last compilation.' - - UNION ALL - SELECT N'Write Weight', - N'MONEY', - N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Query Type', - N'NVARCHAR(256)', - N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' - - UNION ALL - SELECT N'Query Text', - N'NVARCHAR(4000)', - N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' - - UNION ALL - SELECT N'% Executions (Type)', - N'MONEY', - N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' - - UNION ALL - SELECT N'% CPU (Type)', - N'MONEY', - N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' - - UNION ALL - SELECT N'% Duration (Type)', - N'MONEY', - N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' - - UNION ALL - SELECT N'% Reads (Type)', - N'MONEY', - N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' - - UNION ALL - SELECT N'% Writes (Type)', - N'MONEY', - N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' - - UNION ALL - SELECT N'Total Rows', - N'BIGINT', - N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' - - UNION ALL - SELECT N'Average Rows', - N'MONEY', - N'Average number of rows returned by each execution of the query.' - - UNION ALL - SELECT N'Min Rows', - N'BIGINT', - N'The minimum number of rows returned by any execution of this query.' - - UNION ALL - SELECT N'Max Rows', - N'BIGINT', - N'The maximum number of rows returned by any execution of this query.' - - UNION ALL - SELECT N'MinGrantKB', - N'BIGINT', - N'The minimum memory grant the query received in kb.' - - UNION ALL - SELECT N'MaxGrantKB', - N'BIGINT', - N'The maximum memory grant the query received in kb.' - - UNION ALL - SELECT N'MinUsedGrantKB', - N'BIGINT', - N'The minimum used memory grant the query received in kb.' - - UNION ALL - SELECT N'MaxUsedGrantKB', - N'BIGINT', - N'The maximum used memory grant the query received in kb.' - - UNION ALL - SELECT N'PercentMemoryGrantUsed', - N'MONEY', - N'Result of dividing the maximum grant used by the minimum granted.' - - UNION ALL - SELECT N'AvgMaxMemoryGrant', - N'MONEY', - N'The average maximum memory grant for a query.' - - UNION ALL - SELECT N'# Plans', - N'INT', - N'The total number of execution plans found that match a given query.' - - UNION ALL - SELECT N'# Distinct Plans', - N'INT', - N'The number of distinct execution plans that match a given query. ' - + NCHAR(13) + NCHAR(10) - + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' - - UNION ALL - SELECT N'Created At', - N'DATETIME', - N'Time that the execution plan was last compiled.' - - UNION ALL - SELECT N'Last Execution', - N'DATETIME', - N'The last time that this query was executed.' - - UNION ALL - SELECT N'Query Plan', - N'XML', - N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' - - UNION ALL - SELECT N'Plan Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to the compiled plan this query is a part of.' - - UNION ALL - SELECT N'SQL Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' - - UNION ALL - SELECT N'Query Hash', - N'BINARY(8)', - N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' - - UNION ALL - SELECT N'Warnings', - N'VARCHAR(MAX)', - N'A list of individual warnings generated by this query.' ; - - - - /* Configuration table description */ - SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , - N'100' AS [Default Value] , - N'Executions / Minute' AS [Unit of Measure] , - N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] - - UNION ALL - SELECT N'Parameter Sniffing Variance Percent' , - N'30' , - N'Percent' , - N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' - - UNION ALL - SELECT N'Parameter Sniffing IO Threshold' , - N'100,000' , - N'Logical reads' , - N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' - - UNION ALL - SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' - - UNION ALL - SELECT N'Long Running Query Warning' AS [Configuration Parameter] , - N'300' , - N'Seconds' , - N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' - - UNION ALL - SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; - RETURN; -END; - -/*Validate version*/ -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @version_msg VARCHAR(8000); - SELECT @version_msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @version_msg; - RETURN; -END; - -/* Set @Top based on sort */ -IF ( - @Top IS NULL - AND LOWER(@SortOrder) IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 5; - END; - -IF ( - @Top IS NULL - AND LOWER(@SortOrder) NOT IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 10; - END; - -/* validate user inputs */ -IF @Top IS NULL - OR @SortOrder IS NULL - OR @QueryFilter IS NULL - OR @Reanalyze IS NULL -BEGIN - RAISERROR(N'Several parameters (@Top, @SortOrder, @QueryFilter, @renalyze) are required. Do not set them to NULL. Please try again.', 16, 1) WITH NOWAIT; - RETURN; -END; - -RAISERROR(N'Checking @MinutesBack validity.', 0, 1) WITH NOWAIT; -IF @MinutesBack IS NOT NULL - BEGIN - IF @MinutesBack > 0 - BEGIN - RAISERROR(N'Setting @MinutesBack to a negative number', 0, 1) WITH NOWAIT; - SET @MinutesBack *=-1; - END; - IF @MinutesBack = 0 - BEGIN - RAISERROR(N'@MinutesBack can''t be 0, setting to -1', 0, 1) WITH NOWAIT; - SET @MinutesBack = -1; - END; - END; - - -RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; - -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults') IS NULL -BEGIN - CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) - ); -END; - -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs') IS NULL -BEGIN - CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); -END; - -DECLARE @DurationFilter_i INT, - @MinMemoryPerQuery INT, - @msg NVARCHAR(4000) ; - - -IF @BringThePain = 1 - BEGIN - RAISERROR(N'You have chosen to bring the pain. Setting top to 2147483647.', 0, 1) WITH NOWAIT; - SET @Top = 2147483647; - END; - -/* Change duration from seconds to milliseconds */ -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; - SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); - END; - -RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; -SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; -IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> '' -BEGIN - RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); - RETURN; -END; -IF (SELECT DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' -BEGIN - RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); - RETURN; -END; - -SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; - -SET @SortOrder = LOWER(@SortOrder); -SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); -SET @SortOrder = REPLACE(@SortOrder, 'executions per minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'executions / minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'xpm', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'recent compilations', 'compiles'); - -RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; -IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', - 'duration', 'avg duration', 'executions', 'avg executions', - 'compiles', 'memory grant', 'avg memory grant', - 'all', 'all avg') - BEGIN - RAISERROR(N'Invalid sort order chosen, reverting to cpu', 0, 1) WITH NOWAIT; - SET @SortOrder = 'cpu'; - END; - -SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); - -SET @QueryFilter = LOWER(@QueryFilter); - -IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') - BEGIN - RAISERROR(N'Invalid query filter chosen. Reverting to all.', 0, 1) WITH NOWAIT; - SET @QueryFilter = 'all'; - END; - -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skip Analysis set to 1, hiding Summary', 0, 1) WITH NOWAIT; - SET @HideSummary = 1; - END; - -IF @Reanalyze = 1 AND OBJECT_ID('tempdb..##bou_BlitzCacheResults') IS NULL - BEGIN - RAISERROR(N'##bou_BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; - SET @Reanalyze = 0; - END; - -IF @Reanalyze = 0 - BEGIN - RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheResults - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - END; - -IF @Reanalyze = 1 - BEGIN - RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; - GOTO Results; - END; - -IF @SortOrder IN ('all', 'all avg') - BEGIN - RAISERROR(N'Checking all sort orders, please be patient', 0, 1) WITH NOWAIT; - GOTO AllSorts; - END; - - -RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; -IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL - DROP TABLE #only_query_hashes ; - -IF OBJECT_ID('tempdb..#ignore_query_hashes') IS NOT NULL - DROP TABLE #ignore_query_hashes ; - -IF OBJECT_ID('tempdb..#only_sql_handles') IS NOT NULL - DROP TABLE #only_sql_handles ; - -IF OBJECT_ID('tempdb..#ignore_sql_handles') IS NOT NULL - DROP TABLE #ignore_sql_handles ; - -IF OBJECT_ID('tempdb..#p') IS NOT NULL - DROP TABLE #p; - -IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL - DROP TABLE #checkversion; - -IF OBJECT_ID ('tempdb..#configuration') IS NOT NULL - DROP TABLE #configuration; - -IF OBJECT_ID ('tempdb..#stored_proc_info') IS NOT NULL - DROP TABLE #stored_proc_info; - -IF OBJECT_ID ('tempdb..#plan_creation') IS NOT NULL - DROP TABLE #plan_creation; - -IF OBJECT_ID ('tempdb..#est_rows') IS NOT NULL - DROP TABLE #est_rows; - -IF OBJECT_ID ('tempdb..#plan_cost') IS NOT NULL - DROP TABLE #plan_cost; - -IF OBJECT_ID ('tempdb..#proc_costs') IS NOT NULL - DROP TABLE #proc_costs; - -IF OBJECT_ID ('tempdb..#stats_agg') IS NOT NULL - DROP TABLE #stats_agg; - -IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL - DROP TABLE #trace_flags; - -IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL - DROP TABLE #variable_info - -IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL - DROP TABLE #conversion_info - - -IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL - DROP TABLE #missing_index_xml - -IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL - DROP TABLE #missing_index_schema - -IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL - DROP TABLE #missing_index_usage - -IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL - DROP TABLE #missing_index_detail - -IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL - DROP TABLE #missing_index_pretty - - -CREATE TABLE #only_query_hashes ( - query_hash BINARY(8) -); - -CREATE TABLE #ignore_query_hashes ( - query_hash BINARY(8) -); - -CREATE TABLE #only_sql_handles ( - sql_handle VARBINARY(64) -); - -CREATE TABLE #ignore_sql_handles ( - sql_handle VARBINARY(64) -); - -CREATE TABLE #p ( - SqlHandle VARBINARY(64), - TotalCPU BIGINT, - TotalDuration BIGINT, - TotalReads BIGINT, - TotalWrites BIGINT, - ExecutionCount BIGINT -); - -CREATE TABLE #checkversion ( - version NVARCHAR(128), - common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), - major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), - minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), - build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), - revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) -); - -CREATE TABLE #configuration ( - parameter_name VARCHAR(100), - value DECIMAL(38,0) -); - -CREATE TABLE #stored_proc_info -( - SPID INT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - variable_name NVARCHAR(128), - variable_datatype NVARCHAR(128), - converted_column_name NVARCHAR(128), - compile_time_value NVARCHAR(128), - proc_name NVARCHAR(300), - column_name NVARCHAR(128), - converted_to NVARCHAR(128) -); - -CREATE TABLE #plan_creation -( - percent_24 DECIMAL(5, 2), - percent_4 DECIMAL(5, 2), - percent_1 DECIMAL(5, 2), - total_plans INT, - SPID INT -); - -CREATE TABLE #est_rows -( - QueryHash BINARY(8), - estimated_rows FLOAT -); - -CREATE TABLE #plan_cost -( - QueryPlanCost FLOAT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - QueryPlanHash BINARY(8) -); - -CREATE TABLE #proc_costs -( - PlanTotalQuery FLOAT, - PlanHandle VARBINARY(64), - SqlHandle VARBINARY(64) -); - -CREATE TABLE #stats_agg -( - SqlHandle VARBINARY(64), - LastUpdate DATETIME2(7), - ModificationCount INT, - SamplingPercent FLOAT, - [Statistics] NVARCHAR(256), - [Table] NVARCHAR(256), - [Schema] NVARCHAR(256), - [Database] NVARCHAR(256), -); - -CREATE TABLE #trace_flags -( - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - global_trace_flags VARCHAR(1000), - session_trace_flags VARCHAR(1000) -); - -CREATE TABLE #variable_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - variable_name NVARCHAR(200), - variable_datatype NVARCHAR(128), - compile_time_value NVARCHAR(4000) -); - -CREATE TABLE #conversion_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - expression NVARCHAR(4000), - at_charindex AS CHARINDEX('@', expression), - bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), - comma_charindex AS CHARINDEX(',', expression) + 1, - second_comma_charindex AS - CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, - equal_charindex AS CHARINDEX('=', expression) + 1, - paren_charindex AS CHARINDEX('(', expression) + 1, - comma_paren_charindex AS - CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, - convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression) -); - - -CREATE TABLE #missing_index_xml -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - index_xml XML -); - - -CREATE TABLE #missing_index_schema -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - index_xml XML -); - - -CREATE TABLE #missing_index_usage -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - index_xml XML -); - - -CREATE TABLE #missing_index_detail -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - column_name NVARCHAR(128) -); - - -CREATE TABLE #missing_index_pretty -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), - details AS N'/* ' - + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' - + CONVERT(NVARCHAR(30), impact) - + '%.' - + CHAR(10) - + N'*/' - + CHAR(10) + CHAR(13) - + N'/* ' - + CHAR(10) - + N'USE ' - + database_name - + CHAR(10) - + N'GO' - + CHAR(10) + CHAR(13) - + N'CREATE NONCLUSTERED INDEX ix_' - + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') - + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END - + CHAR(10) - + N' ON ' - + schema_name - + N'.' - + table_name - + N' (' + - + CASE WHEN equality IS NOT NULL - THEN equality - + CASE WHEN inequality IS NOT NULL - THEN N', ' + inequality - ELSE N'' - END - ELSE inequality - END - + N')' - + CHAR(10) - + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' - END - + CHAR(10) - + N'GO' - + CHAR(10) - + N'*/' -); - -RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; -WITH x AS ( -SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 4 THEN 1 ELSE 0 END) AS [plans_4], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 1 THEN 1 ELSE 0 END) AS [plans_1], - COUNT(deqs.creation_time) AS [total_plans] -FROM sys.dm_exec_query_stats AS deqs -) -INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) -SELECT CONVERT(DECIMAL(3,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], - x.total_plans, - @@SPID AS SPID -FROM x -OPTION (RECOMPILE) ; - - -SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; -SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; -SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; - -DECLARE @individual VARCHAR(100) ; - -IF (@OnlySqlHandles IS NOT NULL AND @IgnoreSqlHandles IS NOT NULL) -BEGIN -RAISERROR('You shouldn''t need to ignore and filter on SqlHandle at the same time.', 0, 1) WITH NOWAIT; -RETURN; -END; - -IF (@StoredProcName IS NOT NULL AND (@OnlySqlHandles IS NOT NULL OR @IgnoreSqlHandles IS NOT NULL)) -BEGIN -RAISERROR('You can''t filter on stored procedure name and SQL Handle.', 0, 1) WITH NOWAIT; -RETURN; -END; - -IF @OnlySqlHandles IS NOT NULL - AND LEN(@OnlySqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@OnlySqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @OnlySqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlySqlHandles, 0, PATINDEX('%,%',@OnlySqlHandles)) ; - - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @OnlySqlHandles = SUBSTRING(@OnlySqlHandles, LEN(@individual + ',') + 1, LEN(@OnlySqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @OnlySqlHandles; - SET @OnlySqlHandles = NULL; - - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -IF @IgnoreSqlHandles IS NOT NULL - AND LEN(@IgnoreSqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles To Ignore', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@IgnoreSqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreSqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreSqlHandles, 0, PATINDEX('%,%',@IgnoreSqlHandles)) ; - - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @IgnoreSqlHandles = SUBSTRING(@IgnoreSqlHandles, LEN(@individual + ',') + 1, LEN(@IgnoreSqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreSqlHandles; - SET @IgnoreSqlHandles = NULL; - - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' - -BEGIN - RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; - INSERT #only_sql_handles - ( sql_handle ) - SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) - FROM sys.dm_exec_procedure_stats AS deps - WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName - OPTION (RECOMPILE) ; - - IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 - BEGIN - RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; - RETURN; - END; - -END; - - - -IF ((@OnlyQueryHashes IS NOT NULL AND LEN(@OnlyQueryHashes) > 0) - OR (@IgnoreQueryHashes IS NOT NULL AND LEN(@IgnoreQueryHashes) > 0)) - AND LEFT(@QueryFilter, 3) IN ('pro', 'fun') -BEGIN - RAISERROR('You cannot limit by query hash and filter by stored procedure', 16, 1); - RETURN; -END; - -/* If the user is attempting to limit by query hash, set up the - #only_query_hashes temp table. This will be used to narrow down - results. - - Just a reminder: Using @OnlyQueryHashes will ignore stored - procedures and triggers. - */ -IF @OnlyQueryHashes IS NOT NULL - AND LEN(@OnlyQueryHashes) > 0 -BEGIN - RAISERROR(N'Setting up filter for Query Hashes', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@OnlyQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @OnlyQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlyQueryHashes, 0, PATINDEX('%,%',@OnlyQueryHashes)) ; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @OnlyQueryHashes = SUBSTRING(@OnlyQueryHashes, LEN(@individual + ',') + 1, LEN(@OnlyQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @OnlyQueryHashes; - SET @OnlyQueryHashes = NULL; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -/* If the user is setting up a list of query hashes to ignore, those - values will be inserted into #ignore_query_hashes. This is used to - exclude values from query results. - - Just a reminder: Using @IgnoreQueryHashes will ignore stored - procedures and triggers. - */ -IF @IgnoreQueryHashes IS NOT NULL - AND LEN(@IgnoreQueryHashes) > 0 -BEGIN - RAISERROR(N'Setting up filter to ignore query hashes', 0, 1) WITH NOWAIT; - SET @individual = '' ; - - WHILE LEN(@IgnoreQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreQueryHashes, 0, PATINDEX('%,%',@IgnoreQueryHashes)) ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - SET @IgnoreQueryHashes = SUBSTRING(@IgnoreQueryHashes, LEN(@individual + ',') + 1, LEN(@IgnoreQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreQueryHashes ; - SET @IgnoreQueryHashes = NULL ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - END; - END; -END; - -IF @ConfigurationDatabaseName IS NOT NULL -BEGIN - RAISERROR(N'Reading values from Configuration Database', 0, 1) WITH NOWAIT; - DECLARE @config_sql NVARCHAR(MAX) = N'INSERT INTO #configuration SELECT parameter_name, value FROM ' - + QUOTENAME(@ConfigurationDatabaseName) - + '.' + QUOTENAME(@ConfigurationSchemaName) - + '.' + QUOTENAME(@ConfigurationTableName) - + ' ; ' ; - EXEC(@config_sql); -END; - -RAISERROR(N'Setting up variables', 0, 1) WITH NOWAIT; -DECLARE @sql NVARCHAR(MAX) = N'', - @insert_list NVARCHAR(MAX) = N'', - @plans_triggers_select_list NVARCHAR(MAX) = N'', - @body NVARCHAR(MAX) = N'', - @body_where NVARCHAR(MAX) = N'WHERE 1 = 1 ' + @nl, - @body_order NVARCHAR(MAX) = N'ORDER BY #sortable# DESC OPTION (RECOMPILE) ', - - @q NVARCHAR(1) = N'''', - @pv VARCHAR(20), - @pos TINYINT, - @v DECIMAL(6,2), - @build INT; - - -RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; - -INSERT INTO #checkversion (version) -SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) -OPTION (RECOMPILE); - - -SELECT @v = common_version , - @build = build -FROM #checkversion -OPTION (RECOMPILE); - -IF (@SortOrder IN ('memory grant', 'avg memory grant')) -AND ((@v < 11) -OR (@v = 11 AND @build < 6020) -OR (@v = 12 AND @build < 5000) -OR (@v = 13 AND @build < 1601)) -BEGIN - RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); - RETURN; -END; - -IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) -BEGIN - RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); - RETURN; -END; - -RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; - -SET @insert_list += N' -INSERT INTO ##bou_BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, - PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, - ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, - LastExecutionTime, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, - LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, - QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, - TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, - min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime) ' ; - -SET @body += N' -FROM (SELECT TOP (@Top) x.*, xpa.*, - CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY) as age_minutes, - CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY) as age_minutes_lifetime - FROM sys.#view# x - CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa - WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; - -SET @body += N' WHERE 1 = 1 ' + @nl ; - - -IF @IgnoreSystemDBs = 1 - BEGIN - RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; - SET @body += N' AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - END; - -IF @DatabaseName IS NOT NULL OR @DatabaseName <> '' - BEGIN - RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; - SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(' - + QUOTENAME(@DatabaseName, N'''') - + N') ' + @nl; - END; - -IF (SELECT COUNT(*) FROM #only_sql_handles) > 0 -BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; - -IF (SELECT COUNT(*) FROM #ignore_sql_handles) > 0 -BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; - -IF (SELECT COUNT(*) FROM #only_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #only_sql_handles) = 0 - AND (SELECT COUNT(*) FROM #ignore_sql_handles) = 0 -BEGIN - RAISERROR(N'Including only chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_query_hashes q WHERE q.query_hash = x.query_hash) ' + @nl ; -END; - -/* filtering for query hashes */ -IF (SELECT COUNT(*) FROM #ignore_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 -BEGIN - RAISERROR(N'Excluding chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_query_hashes iq WHERE iq.query_hash = x.query_hash) ' + @nl ; -END; -/* end filtering for query hashes */ - - -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; - SET @body += N' AND (total_elapsed_time / 1000.0) / execution_count > @min_duration ' + @nl ; - END; - -IF @MinutesBack IS NOT NULL - BEGIN - RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; - SET @body += N' AND x.last_execution_time >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; - END; - -/* Apply the sort order here to only grab relevant plans. - This should make it faster to process since we'll be pulling back fewer - plans for processing. - */ -RAISERROR(N'Applying chosen sort order', 0, 1) WITH NOWAIT; -SELECT @body += N' ORDER BY ' + - CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY))) AS money) - END ' - END + N' DESC ' + @nl ; - - - -SET @body += N') AS qs - CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, - SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, - SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, - SUM(CAST(total_logical_reads AS BIGINT)) AS t_TotalReads, - SUM(CAST(total_logical_writes AS BIGINT)) AS t_TotalWrites - FROM sys.#view#) AS t - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; - -SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; - - - -SET @plans_triggers_select_list += N' -SELECT TOP (@Top) - @@SPID , - ''Procedure or Function: '' + COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''') AS QueryType, - COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_worker_time / 1000.0) / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_elapsed_time / 1000.0) / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CASE WHEN t.t_TotalExecs = 0 THEN 0 - ELSE CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) - END AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.cached_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - NULL AS StatementStartOffset, - NULL AS StatementEndOffset, - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, - st.text AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - NULL AS QueryHash, - NULL AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_elapsed_time / 1000.0, - age_minutes, - age_minutes_lifetime '; - - -IF LEFT(@QueryFilter, 3) IN ('all', 'sta') -BEGIN - SET @sql += @insert_list; - - SET @sql += N' - SELECT TOP (@Top) - @@SPID , - ''Statement'' AS QueryType, - COALESCE(DB_NAME(CAST(pa.value AS INT)), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_worker_time / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_elapsed_time / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.creation_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - qs.statement_start_offset AS StatementStartOffset, - qs.statement_end_offset AS StatementEndOffset, '; - - IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) - BEGIN - RAISERROR(N'Adding additional info columns for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - qs.min_rows AS MinReturnedRows, - qs.max_rows AS MaxReturnedRows, - CAST(qs.total_rows as MONEY) / execution_count AS AvgReturnedRows, - qs.total_rows AS TotalReturnedRows, - qs.last_rows AS LastReturnedRows, ' ; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for more info columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, ' ; - END; - - IF (@v = 11 AND @build >= 6020) OR (@v = 12 AND @build >= 5000) OR (@v = 13 AND @build >= 1601) - - BEGIN - RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - min_grant_kb AS MinGrantKB, - max_grant_kb AS MaxGrantKB, - min_used_grant_kb AS MinUsedGrantKB, - max_used_grant_kb AS MaxUsedGrantKB, - CAST(ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, - CAST(ISNULL(NULLIF(( max_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for memory grant columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, ' ; - END; - - - SET @sql += N' - SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - qs.query_hash AS QueryHash, - qs.query_plan_hash AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_worker_time / 1000.0, - age_minutes, - age_minutes_lifetime '; - - SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; - - SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; - - SET @sql += @body_order + @nl + @nl + @nl; - - IF @SortOrder = 'compiles' - BEGIN - RAISERROR(N'Sorting by compiles', 0, 1) WITH NOWAIT; - SET @sql = REPLACE(@sql, '#sortable#', 'creation_time'); - END; -END; - - -IF (@QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'pro') -BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Stored Procedure') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_procedure_stats') ; - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - -IF (@v >= 13 - AND @QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'fun') -BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - - -/******************************************************************************* - * - * Because the trigger execution count in SQL Server 2008R2 and earlier is not - * correct, we ignore triggers for these versions of SQL Server. If you'd like - * to include trigger numbers, just know that the ExecutionCount, - * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for - * triggers on these versions of SQL Server. - * - * This is why we can't have nice things. - * - ******************************************************************************/ -IF (@UseTriggersAnyway = 1 OR @v >= 11) - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (@QueryFilter = 'all') - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) -BEGIN - RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; - - /* Trigger level information from the plan cache */ - SET @sql += @insert_list ; - - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; - - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - -DECLARE @sort NVARCHAR(MAX); - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - -SET @sql += N' -INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) -SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount -FROM (SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount, - ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn - FROM ##bou_BlitzCacheProcs) AS x -WHERE x.rn = 1 -OPTION (RECOMPILE); -'; - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' - WHEN N'reads' THEN N'TotalReads' - WHEN N'writes' THEN N'TotalWrites' - WHEN N'duration' THEN N'TotalDuration' - WHEN N'executions' THEN N'ExecutionCount' - WHEN N'compiles' THEN N'PlanCreationTime' - WHEN N'memory grant' THEN N'MaxGrantKB' - WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' - WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' - WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' - WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' - WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' - WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - -IF @Reanalyze = 0 -BEGIN - RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; - - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT', @Top, @DurationFilter_i, @MinutesBack; -END; - - -/* Update ##bou_BlitzCacheProcs to get Stored Proc info - * This should get totals for all statements in a Stored Proc - */ -RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; -;WITH agg AS ( - SELECT - b.SqlHandle, - SUM(b.MinReturnedRows) AS MinReturnedRows, - SUM(b.MaxReturnedRows) AS MaxReturnedRows, - SUM(b.AverageReturnedRows) AS AverageReturnedRows, - SUM(b.TotalReturnedRows) AS TotalReturnedRows, - SUM(b.LastReturnedRows) AS LastReturnedRows, - SUM(b.MinGrantKB) AS MinGrantKB, - SUM(b.MaxGrantKB) AS MaxGrantKB, - SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, - SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB - FROM ##bou_BlitzCacheProcs b - WHERE b.SPID = @@SPID - AND b.QueryHash IS NOT NULL - GROUP BY b.SqlHandle -) -UPDATE b - SET - b.MinReturnedRows = b2.MinReturnedRows, - b.MaxReturnedRows = b2.MaxReturnedRows, - b.AverageReturnedRows = b2.AverageReturnedRows, - b.TotalReturnedRows = b2.TotalReturnedRows, - b.LastReturnedRows = b2.LastReturnedRows, - b.MinGrantKB = b2.MinGrantKB, - b.MaxGrantKB = b2.MaxGrantKB, - b.MinUsedGrantKB = b2.MinUsedGrantKB, - b.MaxUsedGrantKB = b2.MaxUsedGrantKB -FROM ##bou_BlitzCacheProcs b -JOIN agg b2 -ON b2.SqlHandle = b.SqlHandle -WHERE b.QueryHash IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE) ; - -/* Compute the total CPU, etc across our active set of the plan cache. - * Yes, there's a flaw - this doesn't include anything outside of our @Top - * metric. - */ -RAISERROR('Computing CPU, duration, read, and write metrics', 0, 1) WITH NOWAIT; -DECLARE @total_duration BIGINT, - @total_cpu BIGINT, - @total_reads BIGINT, - @total_writes BIGINT, - @total_execution_count BIGINT; - -SELECT @total_cpu = SUM(TotalCPU), - @total_duration = SUM(TotalDuration), - @total_reads = SUM(TotalReads), - @total_writes = SUM(TotalWrites), - @total_execution_count = SUM(ExecutionCount) -FROM #p -OPTION (RECOMPILE) ; - -DECLARE @cr NVARCHAR(1) = NCHAR(13); -DECLARE @lf NVARCHAR(1) = NCHAR(10); -DECLARE @tab NVARCHAR(1) = NCHAR(9); - -/* Update CPU percentage for stored procedures */ -RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT PlanHandle, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE PlanHandle IS NOT NULL - AND SPID = @@SPID - GROUP BY PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.PlanHandle = y.PlanHandle - AND ##bou_BlitzCacheProcs.PlanHandle IS NOT NULL - AND ##bou_BlitzCacheProcs.SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.SqlHandle = y.SqlHandle - AND ##bou_BlitzCacheProcs.QueryHash = y.QueryHash - AND ##bou_BlitzCacheProcs.DatabaseName = y.DatabaseName - AND ##bou_BlitzCacheProcs.PlanHandle IS NULL -OPTION (RECOMPILE) ; - - - -/* Testing using XML nodes to speed up processing */ -RAISERROR(N'Begin XML nodes processing', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -INTO #statements -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS query_plan -INTO #query_plan -FROM #statements p - CROSS APPLY p.statement.nodes('//p:QueryPlan') AS q(n) -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS relop -INTO #relop -FROM #query_plan p - CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) -OPTION (RECOMPILE) ; - - - --- high level plan stuff -RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans , - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END -FROM ( - SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; - --- statement level checks -RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET compile_timeout = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET compile_memory_limit_exceeded = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -unparameterized_query AS ( - SELECT s.QueryHash, - unparameterized_query = CASE WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 1 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList/p:ColumnReference') = 0 THEN 1 - WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 0 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/*/p:RelOp/descendant::p:ScalarOperator/p:Identifier/p:ColumnReference[contains(@Column, "@")]') = 1 THEN 1 - END - FROM #statements AS s - ) -UPDATE b -SET b.unparameterized_query = u.unparameterized_query -FROM ##bou_BlitzCacheProcs b -JOIN unparameterized_query u -ON u.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE u.unparameterized_query = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -index_dml AS ( - SELECT s.QueryHash, - index_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 - END - FROM #statements s - ) - UPDATE b - SET b.index_dml = i.index_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN index_dml i - ON i.QueryHash = b.QueryHash - WHERE i.index_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); - -RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -table_dml AS ( - SELECT s.QueryHash, - table_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 - END - FROM #statements AS s - ) - UPDATE b - SET b.table_dml = t.table_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN table_dml t - ON t.QueryHash = b.QueryHash - WHERE t.table_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); - - -RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT INTO #est_rows -SELECT DISTINCT - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows -FROM #statements AS s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) -WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; - - UPDATE b - SET b.estimated_rows = er.estimated_rows - FROM ##bou_BlitzCacheProcs AS b - JOIN #est_rows er - ON er.QueryHash = b.QueryHash - WHERE b.SPID = @@SPID - AND b.QueryType = 'Statement' - OPTION (RECOMPILE); - - ---Gather costs -RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #plan_cost -SELECT DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, - s.SqlHandle, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash -FROM #statements s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS q(n) -WHERE statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 -OPTION (RECOMPILE); - -RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; -WITH pc AS ( - SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash - FROM #plan_cost AS pc - GROUP BY pc.QueryHash, pc.QueryPlanHash -) - UPDATE b - SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) - FROM pc - JOIN ##bou_BlitzCacheProcs b - ON b.QueryPlanHash = pc.QueryPlanHash - OR b.QueryHash = pc.QueryHash - WHERE b.QueryType NOT LIKE '%Procedure%' - OPTION (RECOMPILE); - -IF EXISTS ( -SELECT 1 -FROM ##bou_BlitzCacheProcs AS b -WHERE b.QueryType LIKE 'Procedure%' -) - -BEGIN - -RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, QueryCost AS ( - SELECT - DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') AS SubTreeCost, - s.PlanHandle, - s.SqlHandle - FROM #statements AS s - WHERE PlanHandle IS NOT NULL -) -, QueryCostUpdate AS ( - SELECT - SUM(qc.SubTreeCost) OVER (PARTITION BY SqlHandle, PlanHandle) PlanTotalQuery, - qc.PlanHandle, - qc.SqlHandle - FROM QueryCost qc -) -INSERT INTO #proc_costs -SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle -FROM QueryCostUpdate AS qcu; - - -UPDATE b - SET b.QueryPlanCost = ca.PlanTotalQuery -FROM ##bou_BlitzCacheProcs AS b -CROSS APPLY ( - SELECT TOP 1 PlanTotalQuery - FROM #proc_costs qcu - WHERE qcu.PlanHandle = b.PlanHandle - ORDER BY PlanTotalQuery DESC -) ca -WHERE b.QueryType LIKE 'Procedure%' -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -END; - -UPDATE b -SET b.QueryPlanCost = 0.0 -FROM ##bou_BlitzCacheProcs b -WHERE b.QueryPlanCost IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET plan_warnings = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET implicit_conversions = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 -OPTION (RECOMPILE); - --- operator level checks -RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , - relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions - FROM #relop qs - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT r.SqlHandle, - 1 AS tvf_join - FROM #relop AS r - WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 - AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, - c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , - c.n.exist('//p:Warnings') AS relop_warnings -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) -) -UPDATE p -SET p.warning_no_join_predicate = x.warning_no_join_predicate, - p.no_stats_warning = x.no_stats_warning, - p.relop_warnings = x.relop_warnings -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char -FROM #relop r -CROSS APPLY r.relop.nodes('//p:Object') AS c(n) -) -UPDATE p -SET is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -) -UPDATE p -SET p.function_count = x.function_count, - p.clr_function_count = x.clr_function_count -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET key_lookup_cost = x.key_lookup_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET remote_query_cost = x.remote_query_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET sort_cost = (x.sort_io + x.sort_cpu) -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM ##bou_BlitzCacheProcs b -JOIN #statements AS qs -ON b.SqlHandle = qs.SqlHandle -CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET -b.is_table_scan = x.is_table_scan, -b.backwards_scan = x.backwards_scan, -b.forced_index = x.forced_index, -b.forced_seek = x.forced_seek, -b.forced_scan = x.forced_scan -FROM ##bou_BlitzCacheProcs b -JOIN ( -SELECT - qs.SqlHandle, - 0 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:IndexScan') AS q(n) -UNION ALL -SELECT - qs.SqlHandle, - 1 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) -) AS x ON b.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_scalar = x.computed_column_function -FROM ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_filter = x.filter_function -FROM ( -SELECT -r.SqlHandle, -c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) -) x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -IndexOps AS -( - SELECT - r.QueryHash, - c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, - c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, - c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, - c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, - c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, - c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, - c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, - c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, - c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, - c.n.exist('@PhysicalOp[.="Table Delete"]') AS td - FROM #relop AS r - CROSS APPLY r.relop.nodes('/p:RelOp') c(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) -), iops AS -( - SELECT ios.QueryHash, - SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, - SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, - SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, - SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, - SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, - SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, - SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, - SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, - SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count - FROM IndexOps AS ios - WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', - 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', - 'Table Insert', 'Table Delete', 'Table Update') - GROUP BY ios.QueryHash) -UPDATE b -SET b.index_insert_count = iops.index_insert_count, - b.index_update_count = iops.index_update_count, - b.index_delete_count = iops.index_delete_count, - b.cx_insert_count = iops.cx_insert_count, - b.cx_update_count = iops.cx_update_count, - b.cx_delete_count = iops.cx_delete_count, - b.table_insert_count = iops.table_insert_count, - b.table_update_count = iops.table_update_count, - b.table_delete_count = iops.table_delete_count -FROM ##bou_BlitzCacheProcs AS b -JOIN iops ON iops.QueryHash = b.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_spatial = x.is_spatial -FROM ( -SELECT qs.SqlHandle, - 1 AS is_spatial -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) -WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( - 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -, selects -AS ( SELECT s.QueryHash - FROM #statements AS s - WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) -, spools -AS ( SELECT DISTINCT r.QueryHash, - c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, - c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, - c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds -FROM #relop AS r -JOIN selects AS s -ON s.QueryHash = r.QueryHash -CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) -WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 -) -UPDATE b - SET b.index_spool_rows = sp.estimated_rows, - b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM ##bou_BlitzCacheProcs b -JOIN spools sp -ON sp.QueryHash = b.QueryHash -OPTION ( RECOMPILE ); - - -/* 2012+ only */ -IF @v >= 11 -BEGIN - - RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET is_forced_serial = 1 - FROM #query_plan qp - WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle - AND SPID = @@SPID - AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 - AND (##bou_BlitzCacheProcs.is_parallel = 0 OR ##bou_BlitzCacheProcs.is_parallel IS NULL) - OPTION (RECOMPILE); - - - RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET columnstore_row_mode = x.is_row_mode - FROM ( - SELECT - qs.SqlHandle, - relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode - FROM #relop qs - WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 - ) AS x - WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle - AND SPID = @@SPID - OPTION (RECOMPILE) ; - -END; - -/* 2014+ only */ -IF @v >= 12 -BEGIN - RAISERROR('Checking for downlevel cardinality estimators being used on SQL Server 2014.', 0, 1) WITH NOWAIT; - - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; -END ; - -/* 2016+ only */ -IF @v >= 13 -BEGIN - RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; - - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET p.is_row_level = 1 - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 - OPTION (RECOMPILE) ; -END ; - -/* 2017+ only */ -IF @v >= 14 -BEGIN - -RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #stats_agg -SELECT qp.SqlHandle, - x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, - x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] -FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c); - -RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; -WITH stale_stats AS ( - SELECT sa.SqlHandle - FROM #stats_agg AS sa - GROUP BY sa.SqlHandle - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000 -) -UPDATE b -SET stale_stats = 1 -FROM ##bou_BlitzCacheProcs b -JOIN stale_stats os -ON b.SqlHandle = os.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -aj AS ( - SELECT - SqlHandle - FROM #relop AS r - CROSS APPLY r.relop.nodes('//p:RelOp') x(c) - WHERE x.c.exist('@IsAdaptive[.=1]') = 1 -) -UPDATE b -SET b.is_adaptive = 1 -FROM ##bou_BlitzCacheProcs b -JOIN aj -ON b.SqlHandle = aj.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE) ; - -END; - --- query level checks -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') , - SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , - SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), - CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , - CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , - CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , - CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float') -FROM #query_plan qp -WHERE qp.QueryHash = ##bou_BlitzCacheProcs.QueryHash -AND SPID = @@SPID -OPTION (RECOMPILE); - - -/* END Testing using XML nodes to speed up processing */ -RAISERROR(N'Gathering additional plan level information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans, - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END , - is_trivial = CASE WHEN QueryPlan.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 THEN 1 END -FROM ( -SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash -FROM ##bou_BlitzCacheProcs -WHERE SPID = @@SPID -GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; - -/* Update to grab stored procedure name for individual statements */ -RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; -UPDATE p -SET QueryType = QueryType + ' (parent ' + - + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) - + '.' - + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' -FROM ##bou_BlitzCacheProcs p - JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle -WHERE QueryType = 'Statement' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -/* Trace Flag Checks 2014 SP2 and 2016 SP1 only)*/ -IF @v >= 11 -BEGIN -RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, tf_pretty AS ( -SELECT qp.QueryHash, - qp.SqlHandle, - q.n.value('@Value', 'INT') AS trace_flag, - q.n.value('@Scope', 'VARCHAR(10)') AS scope -FROM #query_plan qp -CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) -) -INSERT INTO #trace_flags -SELECT DISTINCT tf1.SqlHandle , tf1.QueryHash, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Global' - FOR XML PATH(N'')), 1, 2, N'' - ) AS global_trace_flags, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Session' - FOR XML PATH(N'')), 1, 2, N'' - ) AS session_trace_flags -FROM tf_pretty AS tf1 -OPTION (RECOMPILE); - -UPDATE p -SET p.trace_flags_session = tf.session_trace_flags -FROM ##bou_BlitzCacheProcs p -JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); -END; - - -RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -is_paul_white_electric AS ( -SELECT 1 AS [is_paul_white_electric], -r.SqlHandle -FROM #relop AS r -CROSS APPLY r.relop.nodes('//p:RelOp') c(n) -WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 -) -UPDATE b -SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM ##bou_BlitzCacheProcs AS b -JOIN is_paul_white_electric ipwe -ON ipwe.SqlHandle = b.SqlHandle -WHERE b.SPID = @@SPID -OPTION (RECOMPILE); - -IF EXISTS ( SELECT 1 - FROM ##bou_BlitzCacheProcs AS bbcp - WHERE bbcp.implicit_conversions = 1 - OR bbcp.QueryType LIKE '%Procedure or Function: %') -BEGIN - -RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; - -RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #variable_info ( SPID, QueryHash, SqlHandle, proc_name, variable_name, variable_datatype, compile_time_value ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - q.n.value('@Column', 'NVARCHAR(128)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(128)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(1000)') AS compile_time_value -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) -WHERE b.SPID = @@SPID -OPTION ( RECOMPILE ); - -RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #conversion_info ( SPID, QueryHash, SqlHandle, proc_name, expression ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - qq.c.value('@Expression', 'NVARCHAR(128)') AS expression -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) -WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 - AND qp.QueryHash IS NOT NULL - AND b.implicit_conversions = 1 -AND b.SPID = @@SPID -OPTION ( RECOMPILE ); - -RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; -INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) -SELECT @@SPID AS SPID, - ci.SqlHandle, - ci.QueryHash, - ci.proc_name, - CASE WHEN ci.at_charindex > 0 - AND ci.bracket_charindex > 0 - THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) - ELSE N'**no_variable**' - END AS variable_name, - N'**no_variable**' AS variable_datatype, - CASE WHEN ci.at_charindex = 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column**' - END AS converted_column_name, - CASE WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex = 0 - THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) - WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex > 0 - THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) - WHEN ci.at_charindex > 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column **' - END AS column_name, - CASE WHEN ci.paren_charindex > 0 - 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 - 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 -FROM #conversion_info AS ci -OPTION ( RECOMPILE ); - -IF EXISTS ( SELECT * - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name ) - BEGIN - RAISERROR(N'Updating variables', 0, 1) WITH NOWAIT; - UPDATE sp - SET sp.variable_datatype = vi.variable_datatype, - sp.compile_time_value = vi.compile_time_value - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); - END - ELSE - BEGIN - RAISERROR(N'Inserting variables', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, variable_name, variable_datatype, compile_time_value, proc_name ) - SELECT vi.SPID, vi.SqlHandle, vi.QueryHash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, vi.proc_name - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - END - -RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; -UPDATE s -SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) - ELSE s.variable_datatype - END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) - ELSE s.converted_to - END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) - WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' THEN - QUOTENAME(compile_time_value, '''') - ELSE s.compile_time_value - END -FROM #stored_proc_info AS s -OPTION(RECOMPILE); - -RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, - CONVERT(XML, - N' 'Statement' - THEN N'The stored procedure ' + spi.proc_name - ELSE N'This ad hoc statement' - END - + N' had the following implicit conversions: ' - + CHAR(10) - + STUFF(( - SELECT DISTINCT - @nl - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN N'The variable ' - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'The compiled value ' - WHEN spi2.column_name LIKE '%Expr%' - THEN 'The expression ' - ELSE N'The column ' - END - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN spi2.variable_name - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN spi2.compile_time_value - - ELSE spi2.column_name - END - + N' has a data type of ' - + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to - ELSE spi2.variable_datatype - END - + N' which caused implicit conversion on the column ' - + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' - THEN spi2.converted_column_name - WHEN spi2.column_name = N'**no_column**' - THEN spi2.converted_column_name - WHEN spi2.converted_column_name = N'**no_column**' - THEN spi2.column_name - WHEN spi2.column_name <> spi2.converted_column_name - THEN spi2.converted_column_name - ELSE spi2.column_name - END - + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'' - WHEN spi2.column_name LIKE '%Expr%' - THEN N'' - WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') - THEN ' with the value ' + RTRIM(spi2.compile_time_value) - ELSE N'' - END - + '.' - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) -UPDATE b -SET b.implicit_conversion_info = pk.implicit_conversion_info -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); - -RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, -CONVERT(XML, - N' N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' - THEN spi2.variable_name + N' = ' - ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' - END - + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' - THEN @nl + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' - WHEN spi2.compile_time_value = N'NULL' - THEN spi2.compile_time_value - ELSE RTRIM(spi2.compile_time_value) - END - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - AND spi2.proc_name <> N'Statement' - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @nl - + N' -- ?>' - ) AS cached_execution_parameters -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) -UPDATE b -SET b.cached_execution_parameters = pk.cached_execution_parameters -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); - - -END; --End implicit conversion information gathering - -UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN '' ELSE b.implicit_conversion_info END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN '' ELSE b.cached_execution_parameters END -FROM ##bou_BlitzCacheProcs AS b -WHERE b.SPID = @@SPID -OPTION(RECOMPILE); - -/*Begin Missing Index*/ - -IF EXISTS - (SELECT 1 FROM ##bou_BlitzCacheProcs AS bbcp WHERE bbcp.missing_index_count > 0 AND bbcp.SPID = @@SPID) - BEGIN - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_xml - SELECT qp.QueryHash, - qp.SqlHandle, - c.mg.value('@Impact', 'FLOAT') AS Impact, - c.mg.query('.') AS cmg - FROM #query_plan AS qp - CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) - WHERE qp.QueryHash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_schema - SELECT mix.QueryHash, mix.SqlHandle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , - c.mi.query('.') - FROM #missing_index_xml AS mix - CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_usage - SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, - c.cg.value('@Usage', 'NVARCHAR(128)'), - c.cg.query('.') - FROM #missing_index_schema ms - CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_detail - SELECT miu.QueryHash, - miu.SqlHandle, - miu.impact, - miu.database_name, - miu.schema_name, - miu.table_name, - miu.usage, - c.c.value('@Name', 'NVARCHAR(128)') - FROM #missing_index_usage AS miu - CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); - - INSERT #missing_index_pretty - SELECT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'EQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INEQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INCLUDE' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] - FROM #missing_index_detail AS m - GROUP BY m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); - - WITH missing AS ( - SELECT mip.QueryHash, - mip.SqlHandle, - CONVERT(XML, - N'' - ) AS full_details - FROM #missing_index_pretty AS mip - GROUP BY mip.QueryHash, mip.SqlHandle, mip.impact - ) - UPDATE bbcp - SET bbcp.missing_indexes = m.full_details - FROM ##bou_BlitzCacheProcs AS bbcp - JOIN missing AS m - ON m.SqlHandle = bbcp.SqlHandle - AND SPID = @@SPID - OPTION(RECOMPILE); - - - END - - UPDATE b - SET b.missing_indexes = - CASE WHEN b.missing_indexes IS NULL - THEN '' - ELSE b.missing_indexes - END - FROM ##bou_BlitzCacheProcs AS b - WHERE b.SPID = @@SPID - OPTION(RECOMPILE); - -/*End Missing Index*/ - - - -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; - GOTO Results ; - END; - - -/* Set configuration values */ -RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; -DECLARE @execution_threshold INT = 1000 , - @parameter_sniffing_warning_pct TINYINT = 30, - /* This is in average reads */ - @parameter_sniffing_io_threshold BIGINT = 100000 , - @ctp_threshold_pct TINYINT = 10, - @long_running_query_warning_seconds BIGINT = 300 * 1000 , - @memory_grant_warning_percent INT = 10; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'frequent execution threshold' = LOWER(parameter_name)) -BEGIN - SELECT @execution_threshold = CAST(value AS INT) - FROM #configuration - WHERE 'frequent execution threshold' = LOWER(parameter_name) ; - - SET @msg = ' Setting "frequent execution threshold" to ' + CAST(@execution_threshold AS VARCHAR(10)) ; - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing variance percent' = LOWER(parameter_name)) -BEGIN - SELECT @parameter_sniffing_warning_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'parameter sniffing variance percent' = LOWER(parameter_name) ; - - SET @msg = ' Setting "parameter sniffing variance percent" to ' + CAST(@parameter_sniffing_warning_pct AS VARCHAR(3)) ; - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing io threshold' = LOWER(parameter_name)) -BEGIN - SELECT @parameter_sniffing_io_threshold = CAST(value AS BIGINT) - FROM #configuration - WHERE 'parameter sniffing io threshold' = LOWER(parameter_name) ; - - SET @msg = ' Setting "parameter sniffing io threshold" to ' + CAST(@parameter_sniffing_io_threshold AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name)) -BEGIN - SELECT @ctp_threshold_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name) ; - - SET @msg = ' Setting "cost threshold for parallelism warning" to ' + CAST(@ctp_threshold_pct AS VARCHAR(3)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'long running query warning (seconds)' = LOWER(parameter_name)) -BEGIN - SELECT @long_running_query_warning_seconds = CAST(value * 1000 AS BIGINT) - FROM #configuration - WHERE 'long running query warning (seconds)' = LOWER(parameter_name) ; - - SET @msg = ' Setting "long running query warning (seconds)" to ' + CAST(@long_running_query_warning_seconds AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'unused memory grant' = LOWER(parameter_name)) -BEGIN - SELECT @memory_grant_warning_percent = CAST(value AS INT) - FROM #configuration - WHERE 'unused memory grant' = LOWER(parameter_name) ; - - SET @msg = ' Setting "unused memory grant" to ' + CAST(@memory_grant_warning_percent AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -DECLARE @ctp INT ; - -SELECT @ctp = NULLIF(CAST(value AS INT), 0) -FROM sys.configurations -WHERE name = 'cost threshold for parallelism' -OPTION (RECOMPILE); - - -/* Update to populate checks columns */ -RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , - parameter_sniffing = CASE WHEN AverageReads > @parameter_sniffing_io_threshold - AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , - near_parallel = CASE WHEN QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, - long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 - WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 - WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, - is_key_lookup_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, - is_sort_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, - is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, - is_forced_serial = CASE WHEN is_forced_serial = 1 THEN 1 END, - is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, - long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 THEN 1 END, - low_cost_high_cpu = CASE WHEN QueryPlanCost < @ctp AND AverageCPU > 500. AND QueryPlanCost * 10 < AverageCPU THEN 1 END, - is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, - is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, - is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 10000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 10000) THEN 1 END -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - - -RAISERROR('Checking for forced parameterization and cursors.', 0, 1) WITH NOWAIT; - -/* Set options checks */ -UPDATE p - SET is_forced_parameterized = CASE WHEN (CAST(pa.value AS INT) & 131072 = 131072) THEN 1 END , - is_forced_plan = CASE WHEN (CAST(pa.value AS INT) & 4 = 4) THEN 1 END , - SetOptions = SUBSTRING( - CASE WHEN (CAST(pa.value AS INT) & 1 = 1) THEN ', ANSI_PADDING' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8 = 8) THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 16 = 16) THEN ', ANSI_WARNINGS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 32 = 32) THEN ', ANSI_NULLS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 64 = 64) THEN ', QUOTED_IDENTIFIER' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute = 'set_options' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - -/* Cursor checks */ -UPDATE p -SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute LIKE '%cursor%' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - - -RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; -/* Populate warnings */ -UPDATE ##bou_BlitzCacheProcs -SET Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; -WITH statement_warnings AS - ( -SELECT DISTINCT - SqlHandle, - Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs b -WHERE SPID = @@SPID -AND QueryType LIKE 'Statement (parent%' - ) -UPDATE b -SET b.Warnings = s.Warnings -FROM ##bou_BlitzCacheProcs AS b -JOIN statement_warnings s -ON b.SqlHandle = s.SqlHandle -WHERE QueryType LIKE 'Procedure or Function%' -AND SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; -WITH plan_handle AS ( -SELECT b.PlanHandle -FROM ##bou_BlitzCacheProcs b - CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp - CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp - WHERE tqp.encrypted = 0 - AND b.SPID = @@SPID - AND (qp.query_plan IS NULL - AND tqp.query_plan IS NOT NULL) -) -UPDATE b -SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' - , 'We couldn''t find a plan for this query. Possible reasons for this include dynamic SQL, RECOMPILE hints, and encrypted code.') -FROM ##bou_BlitzCacheProcs b -LEFT JOIN plan_handle ph ON -b.PlanHandle = ph.PlanHandle -WHERE b.QueryPlan IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET Warnings = 'No warnings detected.' -WHERE Warnings = '' OR Warnings IS NULL -AND SPID = @@SPID -OPTION (RECOMPILE); - - -Results: -IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL -BEGIN - RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; - - /* send results to a table */ - DECLARE @insert_sql NVARCHAR(MAX) = N'' ; - - SET @insert_sql = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + N'(ID bigint NOT NULL IDENTITY(1,1), - ServerName nvarchar(256), - CheckDate DATETIMEOFFSET, - Version nvarchar(256), - QueryType nvarchar(256), - Warnings varchar(max), - DatabaseName sysname, - SerialDesiredMemory float, - SerialRequiredMemory float, - AverageCPU bigint, - TotalCPU bigint, - PercentCPUByType money, - CPUWeight money, - AverageDuration bigint, - TotalDuration bigint, - DurationWeight money, - PercentDurationByType money, - AverageReads bigint, - TotalReads bigint, - ReadWeight money, - PercentReadsByType money, - AverageWrites bigint, - TotalWrites bigint, - WriteWeight money, - PercentWritesByType money, - ExecutionCount bigint, - ExecutionWeight money, - PercentExecutionsByType money,' + N' - ExecutionsPerMinute money, - PlanCreationTime datetime, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime datetime, - PlanHandle varbinary(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' - ELSE ''N/A'' END, - SqlHandle varbinary(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' - ELSE ''N/A'' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryHash binary(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryPlanHash binary(8), - StatementStartOffset int, - StatementEndOffset int, - MinReturnedRows bigint, - MaxReturnedRows bigint, - AverageReturnedRows money, - TotalReturnedRows bigint, - QueryText nvarchar(max), - QueryPlan xml, - NumberOfPlans int, - NumberOfDistinctPlans int, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryPlanCost FLOAT, - CONSTRAINT [PK_' +CAST(NEWID() AS NCHAR(36)) + '] PRIMARY KEY CLUSTERED(ID))'; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql ; - - IF @CheckDateOverride IS NULL - BEGIN - SET @CheckDateOverride = SYSDATETIMEOFFSET(); - END - - - SET @insert_sql =N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + N''') ' - + 'INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + N' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ) ' - + N'SELECT TOP (@Top) ' - + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), N'''') + N', @CheckDateOverride, ' - + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), N'''') + ', ' - + N' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ' - + N' FROM ##bou_BlitzCacheProcs ' - + N' WHERE 1=1 '; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @insert_sql += N' AND ExecutionCount >= @MinimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @insert_sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; - - SET @insert_sql += N' AND SPID = @@SPID '; - - SELECT @insert_sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; - - SET @insert_sql += N' OPTION (RECOMPILE) ; '; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; - - RETURN; -END; -ELSE IF @ExportToExcel = 1 -BEGIN - RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; - - /* excel output */ - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) - OPTION(RECOMPILE); - - SET @sql = N' - SELECT TOP (@Top) - DatabaseName AS [Database Name], - QueryPlanCost AS [Cost], - QueryText, - QueryType AS [Query Type], - Warnings, - ExecutionCount, - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - PercentExecutionsByType AS [% Executions (Type)], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - PercentCPUByType AS [% CPU (Type)], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - PercentDurationByType AS [% Duration (Type)], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - PercentReadsByType AS [% Reads (Type)], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows, - AverageReturnedRows, - MinReturnedRows, - MaxReturnedRows, - MinGrantKB, - MaxGrantKB, - MinUsedGrantKB, - MaxUsedGrantKB, - PercentMemoryGrantUsed, - AvgMaxMemoryGrant, - NumberOfPlans, - NumberOfDistinctPlans, - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - StatementStartOffset, - StatementEndOffset, - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - QueryHash, - QueryPlanHash, - COALESCE(SetOptions, '''') AS [SET Options] - FROM ##bou_BlitzCacheProcs - WHERE 1 = 1 - AND SPID = @@SPID ' + @nl; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; - - SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN ' TotalCPU ' - WHEN 'reads' THEN ' TotalReads ' - WHEN 'writes' THEN ' TotalWrites ' - WHEN 'duration' THEN ' TotalDuration ' - WHEN 'executions' THEN ' ExecutionCount ' - WHEN 'compiles' THEN ' PlanCreationTime ' - WHEN 'memory grant' THEN 'MaxGrantKB' - WHEN 'avg cpu' THEN 'AverageCPU' - WHEN 'avg reads' THEN 'AverageReads' - WHEN 'avg writes' THEN 'AverageWrites' - WHEN 'avg duration' THEN 'AverageDuration' - WHEN 'avg executions' THEN 'ExecutionsPerMinute' - WHEN 'avg memory grant' THEN 'AvgMaxMemoryGrant' - END + N' DESC '; - - SET @sql += N' OPTION (RECOMPILE) ; '; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @minimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; -END; - - -RAISERROR('Displaying analysis of plan cache.', 0, 1) WITH NOWAIT; - -DECLARE @columns NVARCHAR(MAX) = N'' ; - -IF @ExpertMode = 0 -BEGIN - RAISERROR(N'Returning ExpertMode = 0', 0, 1) WITH NOWAIT; - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], - ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Avg Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Avg Writes], - PercentWrites AS [Write Weight], - AverageReturnedRows AS [Average Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - COALESCE(SetOptions, '''') AS [SET Options] '; -END; -ELSE -BEGIN - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], ' + @nl; - - IF @ExpertMode = 2 /* Opserver */ - BEGIN - RAISERROR(N'Returning Expert Mode = 2', 0, 1) WITH NOWAIT; - SET @columns += N' - SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN '', 20'' ELSE '''' END + - CASE WHEN compile_timeout = 1 THEN '', 18'' ELSE '''' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN '', 19'' ELSE '''' END + - CASE WHEN busy_loops = 1 THEN '', 16'' ELSE '''' END + - CASE WHEN is_forced_plan = 1 THEN '', 3'' ELSE '''' END + - CASE WHEN is_forced_parameterized > 0 THEN '', 5'' ELSE '''' END + - CASE WHEN unparameterized_query = 1 THEN '', 23'' ELSE '''' END + - CASE WHEN missing_index_count > 0 THEN '', 10'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_cursor = 1 THEN '', 4'' ELSE '''' END + - CASE WHEN is_parallel = 1 THEN '', 6'' ELSE '''' END + - CASE WHEN near_parallel = 1 THEN '', 7'' ELSE '''' END + - CASE WHEN frequent_execution = 1 THEN '', 1'' ELSE '''' END + - CASE WHEN plan_warnings = 1 THEN '', 8'' ELSE '''' END + - CASE WHEN parameter_sniffing = 1 THEN '', 2'' ELSE '''' END + - CASE WHEN long_running = 1 THEN '', 9'' ELSE '''' END + - CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + - CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + - CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + - CASE WHEN plan_multiple_plans = 1 THEN '', 21'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + - CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + - CASE WHEN is_key_lookup_expensive = 1 THEN '', 26'' ELSE '''' END + - CASE WHEN is_remote_query_expensive = 1 THEN '', 28'' ELSE '''' END + - CASE WHEN trace_flags_session IS NOT NULL THEN '', 29'' ELSE '''' END + - CASE WHEN is_unused_grant = 1 THEN '', 30'' ELSE '''' END + - CASE WHEN function_count > 0 THEN '', 31'' ELSE '''' END + - CASE WHEN clr_function_count > 0 THEN '', 32'' ELSE '''' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN '', 33'' ELSE '''' END + - CASE WHEN is_table_variable = 1 THEN '', 34'' ELSE '''' END + - CASE WHEN no_stats_warning = 1 THEN '', 35'' ELSE '''' END + - CASE WHEN relop_warnings = 1 THEN '', 36'' ELSE '''' END + - CASE WHEN is_table_scan = 1 THEN '', 37'' ELSE '''' END + - CASE WHEN backwards_scan = 1 THEN '', 38'' ELSE '''' END + - CASE WHEN forced_index = 1 THEN '', 39'' ELSE '''' END + - CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '', 40'' ELSE '''' END + - CASE WHEN columnstore_row_mode = 1 THEN '', 41'' ELSE '''' END + - CASE WHEN is_computed_scalar = 1 THEN '', 42'' ELSE '''' END + - CASE WHEN is_sort_expensive = 1 THEN '', 43'' ELSE '''' END + - CASE WHEN is_computed_filter = 1 THEN '', 44'' ELSE '''' END + - CASE WHEN index_ops >= 5 THEN '', 45'' ELSE '''' END + - CASE WHEN is_row_level = 1 THEN '', 46'' ELSE '''' END + - CASE WHEN is_spatial = 1 THEN '', 47'' ELSE '''' END + - CASE WHEN index_dml = 1 THEN '', 48'' ELSE '''' END + - CASE WHEN table_dml = 1 THEN '', 49'' ELSE '''' END + - CASE WHEN long_running_low_cpu = 1 THEN '', 50'' ELSE '''' END + - CASE WHEN low_cost_high_cpu = 1 THEN '', 51'' ELSE '''' END + - CASE WHEN stale_stats = 1 THEN '', 52'' ELSE '''' END + - CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + - CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + - CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + - CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + - CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END - , 2, 200000) AS opserver_warning , ' + @nl ; - END; - - SET @columns += N' ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentExecutionsByType AS [% Executions (Type)], - PercentCPUByType AS [% CPU (Type)], - PercentDurationByType AS [% Duration (Type)], - PercentReadsByType AS [% Reads (Type)], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows AS [Total Rows], - AverageReturnedRows AS [Avg Rows], - MinReturnedRows AS [Min Rows], - MaxReturnedRows AS [Max Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - NumberOfPlans AS [# Plans], - NumberOfDistinctPlans AS [# Distinct Plans], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - CachedPlanSize AS [Cached Plan Size (KB)], - CompileTime AS [Compile Time (ms)], - CompileCPU AS [Compile CPU (ms)], - CompileMemory AS [Compile memory (KB)], - COALESCE(SetOptions, '''') AS [SET Options], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - [SQL Handle More Info], - QueryHash AS [Query Hash], - [Query Hash More Info], - QueryPlanHash AS [Query Plan Hash], - StatementStartOffset, - StatementEndOffset, - [Remove Plan Handle From Cache], - [Remove SQL Handle From Cache]'; -END; - - - -SET @sql = N' -SELECT TOP (@Top) ' + @columns + @nl + N' -FROM ##bou_BlitzCacheProcs -WHERE SPID = @spid ' + @nl; - -IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount ' + @nl; - END; - -IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; - END; - -SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; -SET @sql += N' OPTION (RECOMPILE) ; '; - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - -EXEC sp_executesql @sql, N'@Top INT, @spid INT, @minimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; - -IF @HideSummary = 0 AND @ExportToExcel = 0 -BEGIN - IF @Reanalyze = 0 - BEGIN - RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; - - /* Build summary data */ - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE frequent_execution = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 1, - 100, - 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', - 'Queries are being executed more than ' - + CAST (@execution_threshold AS VARCHAR(5)) - + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE parameter_sniffing = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2, - 50, - 'Parameterization', - 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', - 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; - - /* Forced execution plans */ - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_plan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 3, - 5, - 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', - 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_parameterized = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 5, - 50, - 'Parameterization', - 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', - 'Execution plans have been compiled with forced parameterization.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 6, - 200, - 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', - 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE near_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 7, - 200, - 'Execution Plans', - 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', - 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE plan_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 8, - 50, - 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE long_running = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 9, - 50, - 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', - 'Long running queries have been found. These are queries with an average duration longer than ' - + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) - + ' second(s). These queries should be investigated for additional tuning options.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.missing_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 10, - 50, - 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', - 'Queries found with missing indexes.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.downlevel_estimator = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 13, - 200, - 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', - 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE implicit_conversions = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 14, - 50, - 'Performance', - 'Implicit Conversions', - 'http://brentozar.com/go/implicit', - 'One or more queries are comparing two fields that are not of the same data type.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE busy_loops = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 16, - 10, - 'Performance', - 'Frequently executed operators', - 'http://brentozar.com/blitzcache/busy-loops/', - 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE tvf_join = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 17, - 50, - 'Performance', - 'Joining to table valued functions', - 'http://brentozar.com/blitzcache/tvf-join/', - 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_timeout = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 18, - 50, - 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', - 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_memory_limit_exceeded = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 19, - 50, - 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', - 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE warning_no_join_predicate = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 20, - 10, - 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', - 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE plan_multiple_plans = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 21, - 200, - 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', - 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unmatched_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 22, - 100, - 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', - 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unparameterized_query = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 23, - 100, - 'Parameterization', - 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', - 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_trivial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 24, - 100, - 'Execution Plans', - 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', - 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_forced_serial= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 25, - 10, - 'Execution Plans', - 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', - 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_key_lookup_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 26, - 100, - 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', - 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_remote_query_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 28, - 100, - 'Execution Plans', - 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', - 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.trace_flags_session IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 29, - 100, - 'Trace Flags', - 'Session Level Trace Flags Enabled', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'Someone is enabling session level Trace Flags in a query.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_unused_grant IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 30, - 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', - 'https://www.brentozar.com/blitzcache/unused-memory-grants/', - 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 31, - 100, - 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.clr_function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 32, - 100, - 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; - - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_variable = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 33, - 100, - 'Table Variables detected', - 'Beware nasty side effects', - 'https://www.brentozar.com/blitzcache/table-variables/', - 'All modifications are single threaded, and selects have really low row estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.no_stats_warning = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 35, - 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', - 'https://www.brentozar.com/blitzcache/columns-no-statistics/', - 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.relop_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 36, - 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Check the plan for more details.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 37, - 100, - 'Table Scans', - 'Your database has HEAPs', - 'https://www.brentozar.com/archive/2012/05/video-heaps/', - 'This may not be a problem. Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.backwards_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 38, - 100, - 'Backwards Scans', - 'Indexes are being read backwards', - 'https://www.brentozar.com/blitzcache/backwards-scans/', - 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_index = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 39, - 100, - 'Index forcing', - 'Someone is using hints to force index usage', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans, and will prevent missing index requests.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_seek = 1 - OR p.forced_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 40, - 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.columnstore_row_mode = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 41, - 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', - 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', - 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_scalar = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 42, - 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', - 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', - 'This can cause a whole mess of bad serializartion problems.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_sort_expensive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 43, - 100, - 'Execution Plans', - 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', - 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_filter = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 44, - 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Someone put a Scalar UDF in the WHERE clause!') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_ops >= 5 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 45, - 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', - 'No URL yet', - 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_row_level = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', - 'No URL yet', - 'You may see a lot of confusing junk in your query plan.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spatial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 47, - 200, - 'Spatial Abuse', - 'You hit a Spatial Index', - 'No URL yet', - 'Purely informational.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 48, - 150, - 'Index DML', - 'Indexes were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.table_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 49, - 150, - 'Table DML', - 'Tables were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.long_running_low_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 50, - 150, - 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', - 'No URL yet', - 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.low_cost_high_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 51, - 150, - 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', - 'No URL yet', - 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.stale_stats = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 52, - 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', - 'No URL yet', - 'Ever heard of updating statistics?') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_adaptive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', - 'No URL yet', - 'Joe Sack rules.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_expensive = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 54, - 150, - 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_more_rows = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 55, - 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_bad_estimate = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 56, - 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', - 'No URL yet', - 'This may indicate a performance problem if mismatches occur regularly') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_paul_white_electric = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 998, - 200, - 'Is Paul White Electric?', - 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - - IF @v >= 14 - BEGIN - - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT - @@SPID, - 999, - 200, - 'Database Level Statistics', - 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], - '' AS URL, - 'Consider updating statistics more frequently,' AS [Details] - FROM #stats_agg AS sa - GROUP BY sa.[Database] - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000; - - - END; - - - IF EXISTS (SELECT 1/0 - FROM #plan_creation p - WHERE (p.percent_24 > 0) - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT SPID, - 999, - 254, - 'Plan Cache Information', - 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) - + ' total plans in your cache, with ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) - + '% plans created in the past 24 hours, ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) - + '% created in the past 4 hours, and ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) - + '% created in the past 1 hour.', - '', - 'If these percentages are high, it may be a sign of memory pressure or plan cache instability.' - FROM #plan_creation p ; - - IF @v >= 11 - BEGIN - IF EXISTS (SELECT 1/0 - FROM #trace_flags AS tf - WHERE tf.global_trace_flags IS NOT NULL - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 1000, - 255, - 'Global Trace Flags Enabled', - 'You have Global Trace Flags enabled on your server', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; - END; - - IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr - WHERE bcr.Priority = 2147483646 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2147483646, - 255, - 'Need more help?' , - 'Paste your plan on the internet!', - 'http://pastetheplan.com', - 'This makes it easy to share plans and post them to Q&A sites like https://dba.stackexchange.com/!') ; - - - - IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr - WHERE bcr.Priority = 2147483647 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2147483647, - 255, - 'Thanks for using sp_BlitzCache!' , - 'From Your Community Volunteers', - 'http://FirstResponderKit.org', - 'We hope you found this tool useful. Current version: ' + @Version + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.') ; - - END; - - - SELECT Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - FROM ##bou_BlitzCacheResults - WHERE SPID = @@SPID - GROUP BY Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - ORDER BY Priority ASC, CheckID ASC - OPTION (RECOMPILE); -END; - -IF @Debug = 1 - BEGIN - - SELECT '##bou_BlitzCacheResults' AS table_name, * - FROM ##bou_BlitzCacheResults - OPTION ( RECOMPILE ); - - SELECT '##bou_BlitzCacheProcs' AS table_name, * - FROM ##bou_BlitzCacheProcs - OPTION ( RECOMPILE ); - - SELECT '#statements' AS table_name, * - FROM #statements AS s - OPTION (RECOMPILE); - - SELECT '#query_plan' AS table_name, * - FROM #query_plan AS qp - OPTION (RECOMPILE); - - SELECT '#relop' AS table_name, * - FROM #relop AS r - OPTION (RECOMPILE); - - SELECT '#only_query_hashes' AS table_name, * - FROM #only_query_hashes - OPTION ( RECOMPILE ); - - SELECT '#ignore_query_hashes' AS table_name, * - FROM #ignore_query_hashes - OPTION ( RECOMPILE ); - - SELECT '#only_sql_handles' AS table_name, * - FROM #only_sql_handles - OPTION ( RECOMPILE ); - - SELECT '#ignore_sql_handles' AS table_name, * - FROM #ignore_sql_handles - OPTION ( RECOMPILE ); - - SELECT '#p' AS table_name, * - FROM #p - OPTION ( RECOMPILE ); - - SELECT '#checkversion' AS table_name, * - FROM #checkversion - OPTION ( RECOMPILE ); - - SELECT '#configuration' AS table_name, * - FROM #configuration - OPTION ( RECOMPILE ); - - SELECT '#stored_proc_info' AS table_name, * - FROM #stored_proc_info - OPTION ( RECOMPILE ); - - SELECT '#conversion_info' AS table_name, * - FROM #conversion_info AS ci - OPTION ( RECOMPILE ); - - SELECT '#variable_info' AS table_name, * - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - - SELECT '#plan_creation' AS table_name, * - FROM #plan_creation - OPTION ( RECOMPILE ); - - SELECT '#plan_cost' AS table_name, * - FROM #plan_cost - OPTION ( RECOMPILE ); - - SELECT '#proc_costs' AS table_name, * - FROM #proc_costs - OPTION ( RECOMPILE ); - - SELECT '#stats_agg' AS table_name, * - FROM #stats_agg - OPTION ( RECOMPILE ); - - SELECT '#trace_flags' AS table_name, * - FROM #trace_flags - OPTION ( RECOMPILE ); - - END; - - -RETURN; --Avoid going into the AllSort GOTO - -/*Begin code to sort by all*/ -AllSorts: -RAISERROR('Beginning all sort loop', 0, 1) WITH NOWAIT; - - -IF ( - @Top > 10 - AND @BringThePain = 0 - ) - BEGIN - RAISERROR( - ' - You''ve chosen a value greater than 10 to sort the whole plan cache by. - That can take a long time and harm performance. - Please choose a number <= 10, or set @BringThePain = 1 to signify you understand this might be a bad idea. - ', 0, 1) WITH NOWAIT; - RETURN; - END; - - -IF OBJECT_ID('tempdb..#checkversion_allsort') IS NULL - BEGIN - CREATE TABLE #checkversion_allsort - ( - version NVARCHAR(128), - common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1), - major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), - minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), - build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), - revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) - ); - - INSERT INTO #checkversion_allsort - (version) - SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) - OPTION ( RECOMPILE ); - END; - - -SELECT @v = common_version, - @build = build -FROM #checkversion_allsort -OPTION ( RECOMPILE ); - -IF OBJECT_ID('tempdb.. #bou_allsort') IS NULL - BEGIN - CREATE TABLE #bou_allsort - ( - Id INT IDENTITY(1, 1), - DatabaseName VARCHAR(128), - Cost FLOAT, - QueryText NVARCHAR(MAX), - QueryType NVARCHAR(256), - Warnings VARCHAR(MAX), - QueryPlan XML, - missing_indexes XML, - implicit_conversion_info XML, - cached_execution_parameters XML, - ExecutionCount BIGINT, - ExecutionsPerMinute MONEY, - ExecutionWeight MONEY, - TotalCPU BIGINT, - AverageCPU BIGINT, - CPUWeight MONEY, - TotalDuration BIGINT, - AverageDuration BIGINT, - DurationWeight MONEY, - TotalReads BIGINT, - AverageReads BIGINT, - ReadWeight MONEY, - TotalWrites BIGINT, - AverageWrites BIGINT, - WriteWeight MONEY, - AverageReturnedRows MONEY, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - AvgMaxMemoryGrant MONEY, - PlanCreationTime DATETIME, - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - SqlHandle VARBINARY(64), - SetOptions VARCHAR(MAX), - Pattern NVARCHAR(20) - ); - END; - -DECLARE @AllSortSql NVARCHAR(MAX) = N''; -DECLARE @MemGrant BIT; -SELECT @MemGrant = CASE WHEN ( - ( @v < 11 ) - OR ( - @v = 11 - AND @build < 6020 - ) - OR ( - @v = 12 - AND @build < 5000 - ) - OR ( - @v = 13 - AND @build < 1601 - ) - ) THEN 0 - ELSE 1 - END; - - -IF LOWER(@SortOrder) = 'all' -BEGIN -RAISERROR('Beginning for ALL', 0, 1) WITH NOWAIT; -SET @AllSortSql += N' - DECLARE @ISH NVARCHAR(MAX) = N'''' - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - '; - - IF @MemGrant = 0 - BEGIN - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - - IF @MemGrant = 1 - BEGIN - SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - -END; - - -IF LOWER(@SortOrder) = 'all avg' -BEGIN -RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; -SET @AllSortSql += N' - DECLARE @ISH NVARCHAR(MAX) = N'''' - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - '; - - IF @MemGrant = 0 - BEGIN - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - - IF @MemGrant = 1 - BEGIN - SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; -END; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@AllSortSql, 0, 4000); - PRINT SUBSTRING(@AllSortSql, 4000, 8000); - PRINT SUBSTRING(@AllSortSql, 8000, 12000); - PRINT SUBSTRING(@AllSortSql, 12000, 16000); - PRINT SUBSTRING(@AllSortSql, 16000, 20000); - PRINT SUBSTRING(@AllSortSql, 20000, 24000); - PRINT SUBSTRING(@AllSortSql, 24000, 28000); - PRINT SUBSTRING(@AllSortSql, 28000, 32000); - PRINT SUBSTRING(@AllSortSql, 32000, 36000); - PRINT SUBSTRING(@AllSortSql, 36000, 40000); - END; - - EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT', @i_DatabaseName = @DatabaseName, @i_Top = @Top; - - -/*End of AllSort section*/ - -END; /*Final End*/ - -GO -IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;') -GO - - -ALTER PROCEDURE [dbo].[sp_BlitzFirst] - @LogMessage NVARCHAR(4000) = NULL , - @Help TINYINT = 0 , - @AsOf DATETIMEOFFSET = NULL , - @ExpertMode TINYINT = 0 , - @Seconds INT = 5 , - @OutputType VARCHAR(20) = 'TABLE' , - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @OutputTableNameFileStats NVARCHAR(256) = NULL , - @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , - @OutputTableNameWaitStats NVARCHAR(256) = NULL , - @OutputTableNameBlitzCache NVARCHAR(256) = NULL , - @OutputTableRetentionDays TINYINT = 7 , - @OutputXMLasNVARCHAR TINYINT = 0 , - @FilterPlansByDatabase VARCHAR(MAX) = NULL , - @CheckProcedureCache TINYINT = 0 , - @FileLatencyThresholdMS INT = 100 , - @SinceStartup TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0 , - @LogMessageCheckID INT = 38, - @LogMessagePriority TINYINT = 1, - @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', - @LogMessageFinding VARCHAR(200) = 'Logged from sp_BlitzFirst', - @LogMessageURL VARCHAR(200) = '', - @LogMessageCheckDate DATETIMEOFFSET = NULL, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT - WITH EXECUTE AS CALLER, RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - - -IF @Help = 1 PRINT ' -sp_BlitzFirst from http://FirstResponderKit.org - -This script gives you a prioritized list of why your SQL Server is slow right now. - -This is not an overall health check - for that, check out sp_Blitz. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. It - may work just fine on 2005, and if it does, hug your parents. Just don''t - file support issues if it breaks. - - If a temp table called #CustomPerfmonCounters exists for any other session, - but not our session, this stored proc will fail with an error saying the - temp table #CustomPerfmonCounters does not exist. - - @OutputServerName is not functional yet. - - If @OutputDatabaseName, SchemaName, TableName, etc are quoted with brackets, - the write to table may silently fail. Look, I never said I was good at this. - -Unknown limitations of this version: - - None. Like Zombo.com, the only limit is yourself. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - -MIT License - -Copyright (c) 2017 Brent Ozar Unlimited - -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. - -' - - -RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; -DECLARE @StringToExecute NVARCHAR(MAX), - @ParmDefinitions NVARCHAR(4000), - @Parm1 NVARCHAR(4000), - @OurSessionID INT, - @LineFeed NVARCHAR(10), - @StockWarningHeader NVARCHAR(500), - @StockWarningFooter NVARCHAR(100), - @StockDetailsHeader NVARCHAR(100), - @StockDetailsFooter NVARCHAR(100), - @StartSampleTime DATETIMEOFFSET, - @FinishSampleTime DATETIMEOFFSET, - @FinishSampleTimeWaitFor DATETIME, - @ServiceName sysname, - @OutputTableNameFileStats_View NVARCHAR(256), - @OutputTableNamePerfmonStats_View NVARCHAR(256), - @OutputTableNameWaitStats_View NVARCHAR(256), - @OutputTableNameWaitStats_Categories NVARCHAR(256), - @ObjectFullName NVARCHAR(2000), - @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', - @BlitzCacheMinutesBack INT, - @BlitzCacheSortOrder VARCHAR(50), - @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , - @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , - @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName ; - -/* Sanitize our inputs */ -SELECT - @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), - @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), - @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), - @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); - -SELECT - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), - @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), - @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), - @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), - /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ - @LineFeed = CHAR(13) + CHAR(10), - @StartSampleTime = SYSDATETIMEOFFSET(), - @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), - @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()), - @OurSessionID = @@SPID; - -IF @LogMessage IS NOT NULL - BEGIN - - RAISERROR('Saving LogMessage to table',10,1) WITH NOWAIT; - - /* Try to set the output table parameters if they don't exist */ - IF @OutputSchemaName IS NULL AND @OutputTableName IS NULL AND @OutputDatabaseName IS NULL - BEGIN - SET @OutputSchemaName = N'[dbo]'; - SET @OutputTableName = N'[BlitzFirst]'; - - /* Look for the table in the current database */ - SELECT TOP 1 @OutputDatabaseName = QUOTENAME(TABLE_CATALOG) - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'BlitzFirst'; - - IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') - SET @OutputDatabaseName = '[DBAtools]'; - - END - - IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL - OR NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; - RETURN; - END - IF @LogMessageCheckDate IS NULL - SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @LogMessageCheckDate, 121) + ''', @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)' - - EXECUTE sp_executesql @StringToExecute, - N'@LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', - @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; - - RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; - - RETURN; - END - -IF @SinceStartup = 1 - SELECT @Seconds = 0, @ExpertMode = 1; - -IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT @StartSampleTime = DATEADD(ms, AVG(-wait_time_ms), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.dm_os_wait_stats w - WHERE wait_type IN ('BROKER_TASK_STOP','DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', - 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT') -ELSE IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.databases - WHERE database_id = 2; -ELSE - SELECT @StartSampleTime = SYSDATETIMEOFFSET(), @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()); - -IF @OutputType = 'SCHEMA' -BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)' - -END -ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL -BEGIN - /* They want to look into the past. */ - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT CheckDate, [Priority], [FindingsGroup], [Finding], [URL], CAST([Details] AS [XML]) AS Details,' - + '[HowToStopIt], [CheckID], [StartTime], [LoginName], [NTUserName], [OriginalLoginName], [ProgramName], [HostName], [DatabaseID],' - + '[DatabaseName], [OpenTransactionCount], [QueryPlan], [QueryText] FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE CheckDate >= DATEADD(mi, -15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' AND CheckDate <= DATEADD(mi, 15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; - EXEC(@StringToExecute); - - -END /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ -ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ -BEGIN - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - - - RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; - - /* - We start by creating #BlitzFirstResults. It's a temp table that will store - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. - - #BlitzFirstResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can - download that from http://FirstResponderKit.org if you want to build - a tool that relies on the output of sp_BlitzFirst. - */ - - IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL - DROP TABLE #BlitzFirstResults; - CREATE TABLE #BlitzFirstResults - ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt NVARCHAR(MAX) NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - QueryStatsNowID INT NULL, - QueryStatsFirstID INT NULL, - PlanHandle VARBINARY(64) NULL, - DetailsInt INT NULL, - ); - - IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL - DROP TABLE #WaitStats; - CREATE TABLE #WaitStats (Pass TINYINT NOT NULL, wait_type NVARCHAR(60), wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT, SampleTime DATETIMEOFFSET); - - IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL - DROP TABLE #FileStats; - CREATE TABLE #FileStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - avg_stall_read_ms INT , - avg_stall_write_ms INT - ); - - IF OBJECT_ID('tempdb..#QueryStats') IS NOT NULL - DROP TABLE #QueryStats; - CREATE TABLE #QueryStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass INT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [sql_handle] VARBINARY(64), - statement_start_offset INT, - statement_end_offset INT, - plan_generation_num BIGINT, - plan_handle VARBINARY(64), - execution_count BIGINT, - total_worker_time BIGINT, - total_physical_reads BIGINT, - total_logical_writes BIGINT, - total_logical_reads BIGINT, - total_clr_time BIGINT, - total_elapsed_time BIGINT, - creation_time DATETIMEOFFSET, - query_hash BINARY(8), - query_plan_hash BINARY(8), - Points TINYINT - ); - - IF OBJECT_ID('tempdb..#PerfmonStats') IS NOT NULL - DROP TABLE #PerfmonStats; - CREATE TABLE #PerfmonStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL - ); - - IF OBJECT_ID('tempdb..#PerfmonCounters') IS NOT NULL - DROP TABLE #PerfmonCounters; - CREATE TABLE #PerfmonCounters ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL - ); - - IF OBJECT_ID('tempdb..#FilterPlansByDatabase') IS NOT NULL - DROP TABLE #FilterPlansByDatabase; - CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); - - IF OBJECT_ID('tempdb..##WaitCategories') IS NULL - BEGIN - /* We reuse this one by default rather than recreate it every time. */ - CREATE TABLE ##WaitCategories - ( - WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, - WaitCategory NVARCHAR(128) NOT NULL, - Ignorable BIT DEFAULT 0 - ); - END /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ - - IF 504 <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) - BEGIN - TRUNCATE TABLE ##WaitCategories; - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_NETWORK_IO','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BACKUPIO','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_CONNECTION_RECEIVE_TASK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_DISPATCHER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_ENDPOINT_STATE_MUTEX','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_EVENTHANDLER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_FORWARDER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_INIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_MASTERSTART','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_RECEIVE_WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_REGISTERALLENDPOINTS','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SERVICE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_START','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_STOP','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SUBMIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TO_FLUSH','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_OBJECT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_TABLE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_WORK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_SEND','Mirroring',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_STATE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_TMDOWN_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_WAITFOR_OUTCOME','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_ENLIST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_PREPARE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_RECOVERY','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TM','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TRANSACTION_ENLISTMENT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCPNTSYNC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EE_PMOLOCK','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXCHANGE','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXTERNAL_SCRIPT_NETWORK_IOF','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_READ','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_WRITE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_METADATA_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_PROPERTYLIST_CACHE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_RESTART_CRAWL','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FULLTEXT GATHERER','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AG_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_CRITICAL_SECTION_ENTRY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_MANAGER_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_UNLOAD_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_BULK_LOCK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_QUEUE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CLUSAPI_CALL','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_COMPRESSED_CACHE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CONNECTIVITY_INFO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_VERSIONING_STATE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RECOVERY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RESTART','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_COMMAND','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_COMPLETION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_START_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER_FILTER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_MANAGER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_PREPROC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_GROUP_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_WAIT','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGPROGRESS_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_DEQUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_STARTUP_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_PARTNER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_READ_ALL_NETWORKS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_CONNECTION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_UNDO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_REPLICAINFO_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_CANCELLATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_FILE_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_LIMIT_BACKUPS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_SYNC_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_TIMEOUT_TASK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_WAIT_FOR_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNC_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNCHRONIZING_THROTTLE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC_PROCESSING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_THROTTLE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TIMER_TASK','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_DBRLIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_SESSION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_POOL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_QUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_XRF_STACK_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('INSTANCE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_QUEUE_LIMIT','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_RETRY','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_DT','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_EX','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_KP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_NL','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_SH','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_UP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LAZYWRITER_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_PMM_LOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_RESERVE_APPEND','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_GRANT_UPDATE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MGR_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSSEARCH','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('NET_WAITFOR_PACKET','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ONDEMAND_TASK_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_DT','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_EX','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_KP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_NL','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_SH','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_UP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_DT','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_EX','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_KP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPVDIDEVICE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COCREATEINSTANCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COGETCLASSOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_CREATEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_DELETEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETCOMMANDTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETNEXTROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETRESULT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETROWSBYBOOKMARK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBFLUSH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBREADAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBWRITEAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_QUERYINTERFACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASESESSION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RESTARTPOSITION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREADANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETDATAFAILURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CONSOLEWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CREATEPARAM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DEBUG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSADDLINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKEXISTCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKHEALTHCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVELINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVEROOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTFOLDERCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTSHARECHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORTREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_BEGINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_COMMITREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_PREPAREREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FILESIZEGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_STARTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_GETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_EVENT_WAIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_REQUEST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_LOCKMONITOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_MSS_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ODBCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLE_UNINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETDATASOURCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETLITERALINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETSCHEMALOCK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_JOINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_SETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDBOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHENTICATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHORIZATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_BACKUPREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLOSEHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLUSTEROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMPLETEAUTHTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COPYFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTACQUIRECONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTIMPORTKEY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DECRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEIOCONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DIRSVC_NETWORKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DISCONNECTNAMEDPIPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DOMAINSERVICESOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DSGETDCNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DTCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ENCRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FILEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FINDFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FORMATMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREELIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GENERICOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETADDRINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETDISKFREESPACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILEATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETLONGPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETPROCADDRESS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMEPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LIBRARYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOADLIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOGONUSER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOOKUPACCOUNTSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MESSAGEQUEUEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MOVEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETGROUPGETUSERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETLOCALGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERMODALSGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_OPENDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PDH_WMI_INIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PIPEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PROCESSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYREGISTRY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REMOVEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REPORTEVENT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REVERTTOSELF','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_RSFXDEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SECURITYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SERVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETENDOFFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEPOINTER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEVALIDDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETNAMEDSECURITYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQLCLROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQMLAUNCH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYSIGNATURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYTRUST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VSSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WAITFORSINGLEOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WINSOCKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILEGATHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WSASETLASTERROR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_REENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_RESIZELOG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDREDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SB_STOPENDPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SERVER_STARTUP','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SHAREDMEM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SNIOPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSHOST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSTESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STARTRM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_CHECKPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_RECOVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STRESSDRIVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TRANSIMPORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETFINALIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TIMERRUN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XETESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ACTION_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CLUSTER_INTEGRATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_FAILOVER_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_JOIN','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_OFFLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_POST_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_SERVER_READY_CONNECTIONS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_WORKITEM_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADRSIM','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_ASYNC_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_SHUTDOWN_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QUERY_TRACEOUT','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REDO_THREAD_PENDING_WORK','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_CACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_HISTORYCACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_SCHEMA_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANFSINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANHASHTABLE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANTEXTINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPLICA_WRITES','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REQUEST_FOR_DEADLOCK_SEARCH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESERVED_MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE_QUERY_COMPILE','Compilation',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BPOOL_FLUSH','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BUFFERPOOL_HELPLW','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DCOMSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERDBREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERMDREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERUPGRADED','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MEMORYPOOL_ALLOCATEPAGES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MSDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_RETRY_VIRTUALALLOC','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_SYSTEMTASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_DEADLOCK_DETECTION','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_QUANTUM_PUNISHMENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_BUFFER_FLUSH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_BUFFER','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_READ_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_WRITE_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_PENDING_BUFFER_WRITERS','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_SHUTDOWN','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_WAIT_ENTRIES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('THREADPOOL','Worker Thread',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACE_EVTNOTIF','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACEWRITE','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_DT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_EX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_KP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_NL','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_SH','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_UP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRANSACTION_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('UCS_SESSION_REGISTRATION','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_FOR_RESULTS','User Wait',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITE_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITELOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_OWN_TRANSACTION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_RECLAIM_SESSION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTLOCKINFO','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTWORKSPACE_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); - END /* IF SELECT SUM(1) FROM ##WaitCategories <> 504 */ - - - - IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL - DROP TABLE #MasterFiles; - CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); - /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;' - ELSE - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;' - EXEC(@StringToExecute); - - IF @FilterPlansByDatabase IS NOT NULL - BEGIN - IF UPPER(LEFT(@FilterPlansByDatabase,4)) = 'USER' - BEGIN - INSERT INTO #FilterPlansByDatabase (DatabaseID) - SELECT database_id - FROM sys.databases - WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb') - END - ELSE - BEGIN - SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' - ;WITH a AS - ( - SELECT CAST(1 AS BIGINT) f, CHARINDEX(',', @FilterPlansByDatabase) t, 1 SEQ - UNION ALL - SELECT t + 1, CHARINDEX(',', @FilterPlansByDatabase, t + 1), SEQ + 1 - FROM a - WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 - ) - INSERT #FilterPlansByDatabase (DatabaseID) - SELECT SUBSTRING(@FilterPlansByDatabase, f, t - f) - FROM a - WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL - OPTION (MAXRECURSION 0) - END - END - - - SET @StockWarningHeader = '', - @StockDetailsHeader = ''; - - /* Get the instance name to use as a Perfmon counter prefix. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) - FROM sys.dm_os_performance_counters; - ELSE - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;' - EXEC(@StringToExecute); - SELECT @ServiceName = object_name FROM #PerfmonStats; - DELETE #PerfmonStats; - END - - /* Build a list of queries that were run in the last 10 seconds. - We're looking for the death-by-a-thousand-small-cuts scenario - where a query is constantly running, and it doesn't have that - big of an impact individually, but it has a ton of impact - overall. We're going to build this list, and then after we - finish our @Seconds sample, we'll compare our plan cache to - this list to see what ran the most. */ - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @CheckProcedureCache = 1 - BEGIN - RAISERROR('@CheckProcedureCache = 1, capturing first pass of plan cache',10,1) WITH NOWAIT; - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - EXEC(@StringToExecute); - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - END /*IF @CheckProcedureCache = 1 */ - - - IF EXISTS (SELECT * - FROM tempdb.sys.all_objects obj - INNER JOIN tempdb.sys.all_columns col1 ON obj.object_id = col1.object_id AND col1.name = 'object_name' - INNER JOIN tempdb.sys.all_columns col2 ON obj.object_id = col2.object_id AND col2.name = 'counter_name' - INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' - WHERE obj.name LIKE '%CustomPerfmonCounters%') - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters' - EXEC(@StringToExecute); - END - ELSE - BEGIN - /* Add our default Perfmon counters */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL) - /* Below counters added by Jefferson Elias */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL) - /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. - And yes, they actually hard-coded the version numbers into the counters. - For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group - */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL) - END - - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. - After we finish doing our checks, we'll take another sample and compare them. */ - RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - owt.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - 0 AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - os.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_read_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_reads END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_read] END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_write_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_writes END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_written] END , - mf.physical_name, - mf.type_desc - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), CASE @Seconds WHEN 0 THEN 0 ELSE dmv.cntr_value END, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; - - - /* Maintenance Tasks Running - Backup Running - CheckID 1 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 1 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Backup Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/backups/' AS URL, - 'Backup 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)) + '. ' - + CASE WHEN COALESCE(s.nt_user_name, s.login_name) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_user_name, s.login_name) + ' ') ELSE '' END AS Details, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'BACKUP%' - AND r.start_time <= DATEADD(minute, -5, GETDATE()); - - /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ - IF @Seconds > 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - BEGIN - SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; - EXEC(@StringToExecute); - END - - - /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ - IF @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%') - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 2 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'DBCC CHECK* Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/dbcc/' AS URL, - 'Corruption check 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) 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, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id - FROM sys.dm_tran_locks l - INNER JOIN sys.databases d ON l.resource_database_id = d.database_id - WHERE l.resource_type = N'DATABASE' - AND l.request_mode = N'S' - AND l.request_status = N'GRANT' - AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t - WHERE r.command LIKE 'DBCC%' - AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%'; - - - /* Maintenance Tasks Running - Restore Running - CheckID 3 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 3 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Restore Running' AS Finding, - 'http://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, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'RESTORE%'; - - - /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 4 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Database File Growing' AS Finding, - 'http://www.BrentOzar.com/go/instant' AS URL, - 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, - 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out http://www.BrentOzar.com/go/instant for details.' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - NULL AS DatabaseID, - NULL AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks t - INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id - INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' - - - /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ - IF @@VERSION NOT LIKE '%Azure%' AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) - BEGIN - SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 5 AS CheckID, - 1 AS Priority, - ''Query Problems'' AS FindingGroup, - ''Long-Running Query Blocking Others'' AS Finding, - ''http://www.BrentOzar.com/go/blocking'' AS URL, - ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows:'' ' - + @LineFeed + @LineFeed + - '+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, - ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, - (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, - COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id)) AS QueryText, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - r.[database_id] AS DatabaseID, - DB_NAME(r.database_id) AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks tBlocked - INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id - LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000;' - EXECUTE sp_executesql @StringToExecute; - END - - /* Query Problems - Plan Cache Erased Recently */ - IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT TOP 1 7 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Plan Cache Erased Recently' AS Finding, - 'http://www.BrentOzar.com/askbrent/plan-cache-erased-recently/' AS URL, - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed - + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed - + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed - + 'SQL Server has to use a lot of CPU power in order to build execution' + @LineFeed - + 'plans and put them in cache again. This causes high CPU loads.' AS Details, - 'Find who did that, and stop them from doing it again.' AS HowToStopIt - FROM sys.dm_exec_query_stats - ORDER BY creation_time - END; - - - /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) - SELECT 8 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Sleeping Query with Open Transactions' AS Finding, - 'http://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, - 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.[host_name] + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_request_end_time AS NVARCHAR(100)) + '. ' AS Details, - 'KILL ' + CAST(s.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - s.last_request_start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, - sessions_with_transactions.open_transaction_count AS OpenTransactionCount - FROM (SELECT session_id, SUM(open_transaction_count) AS open_transaction_count FROM sys.dm_exec_requests WHERE open_transaction_count > 0 GROUP BY session_id) AS sessions_with_transactions - INNER JOIN sys.dm_exec_sessions s ON sessions_with_transactions.session_id = s.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE s.status = 'sleeping' - AND s.last_request_end_time < DATEADD(ss, -10, SYSDATETIME()) - AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.session_id - AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')) - - - /* Query Problems - Query Rolling Back - CheckID 9 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText) - SELECT 9 AS CheckID, - 1 AS Priority, - 'Query Problems' AS FindingGroup, - 'Query Rolling Back' AS Finding, - 'http://www.BrentOzar.com/askbrent/rollback/' AS URL, - 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, - 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText - FROM sys.dm_exec_sessions s - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - LEFT OUTER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE r.status = 'rollback' - - - /* Server Performance - Page Life Expectancy Low - CheckID 10 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 10 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Page Life Expectancy Low' AS Finding, - 'http://www.BrentOzar.com/askbrent/page-life-expectancy/' AS URL, - 'SQL Server Buffer Manager:Page life expectancy is ' + CAST(c.cntr_value AS NVARCHAR(10)) + ' seconds.' + @LineFeed - + 'This means SQL Server can only keep data pages in memory for that many seconds after reading those pages in from storage.' + @LineFeed - + 'This is a symptom, not a cause - it indicates very read-intensive queries that need an index, or insufficient server memory.' AS Details, - 'Add more memory to the server, or find the queries reading a lot of data, and make them more efficient (or fix them with indexes).' AS HowToStopIt - FROM sys.dm_os_performance_counters c - WHERE object_name LIKE 'SQLServer:Buffer Manager%' - AND counter_name LIKE 'Page life expectancy%' - AND cntr_value < 300 - - /* Server Performance - Too Much Free Memory - CheckID 34 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 34 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Too Much Free Memory' AS Finding, - 'https://BrentOzar.com/go/freememory' AS URL, - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, - 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 35 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Target Memory Lower Than Max' AS Finding, - 'https://BrentOzar.com/go/target' AS URL, - N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed - + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, - 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt - FROM sys.configurations cMax - INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' - AND cTarget.counter_name = N'Target Server Memory (KB) ' - WHERE cMax.name = 'max server memory (MB)' - AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) - AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ - AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ - - /* Server Info - Database Size, Total GB - CheckID 21 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 21 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Size, Total GB' AS Finding, - CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, - SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM #MasterFiles - WHERE database_id > 4 - - /* Server Info - Database Count - CheckID 22 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 22 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Count' AS Finding, - CAST(SUM(1) AS VARCHAR(100)) AS Details, - SUM (1) AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM sys.databases - WHERE database_id > 4 - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds < 30 - BEGIN - /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - /* Highlight if non SQL processes are using >25% CPU */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - ,record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLUsage - 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 - (y.SQLUsage + y.SystemIdle) >= 25 - - END /* IF @Seconds < 30 */ - - RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; - - - /* End of checks. If we haven't waited @Seconds seconds, wait. */ - IF SYSDATETIMEOFFSET() < @FinishSampleTime - BEGIN - RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; - WAITFOR TIME @FinishSampleTimeWaitFor; - END - - RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - owt.wait_type, - SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - os.wait_type, - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, - SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, - SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - vfs.io_stall_read_ms , - vfs.num_of_reads , - vfs.[num_of_bytes_read], - vfs.io_stall_write_ms , - vfs.num_of_writes , - vfs.[num_of_bytes_written], - mf.physical_name, - mf.type_desc, - 0, - 0 - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), dmv.cntr_value, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ - UPDATE fNow - SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms - WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0 - - UPDATE fNow - SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms - WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0 - - UPDATE pNow - SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, - [value_per_second] = ((1.0 * pNow.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime)) - FROM #PerfmonStats pNow - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pNow.[object_name] AND pFirst.counter_name = pNow.counter_name AND (pFirst.instance_name = pNow.instance_name OR (pFirst.instance_name IS NULL AND pNow.instance_name IS NULL)) - AND pNow.ID > pFirst.ID - WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; - - - /* If we're within 10 seconds of our projected finish time, do the plan cache analysis. */ - IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIME()) > 10 AND @CheckProcedureCache = 1 - BEGIN - - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'http://www.BrentOzar.com/go/topqueries', - 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.') - - END - ELSE IF @CheckProcedureCache = 1 - BEGIN - - - RAISERROR('@CheckProcedureCache = 1, capturing second pass of plan cache',10,1) WITH NOWAIT; - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - /* Old version pre-2016/06/13: - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - ELSE - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - */ - SET @ParmDefinitions = N'@StartSampleTimeText NVARCHAR(100)'; - SET @Parm1 = CONVERT(NVARCHAR(100), CAST(@StartSampleTime AS DATETIME), 127); - - EXECUTE sp_executesql @StringToExecute, @ParmDefinitions, @StartSampleTimeText = @Parm1; - - RAISERROR('@CheckProcedureCache = 1, totaling up plan cache metrics',10,1) WITH NOWAIT; - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT 0 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - - - RAISERROR('@CheckProcedureCache = 1, so analyzing execution plans',10,1) WITH NOWAIT; - /* - Pick the most resource-intensive queries to review. Update the Points field - in #QueryStats - if a query is in the top 10 for logical reads, CPU time, - duration, or execution, add 1 to its points. - */ - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_elapsed_time > qsFirst.total_elapsed_time - AND qsNow.Pass = 2 - AND qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - ORDER BY (qsNow.total_elapsed_time - COALESCE(qsFirst.total_elapsed_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_logical_reads > qsFirst.total_logical_reads - AND qsNow.Pass = 2 - AND qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - ORDER BY (qsNow.total_logical_reads - COALESCE(qsFirst.total_logical_reads, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_worker_time > qsFirst.total_worker_time - AND qsNow.Pass = 2 - AND qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */ - ORDER BY (qsNow.total_worker_time - COALESCE(qsFirst.total_worker_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.execution_count > qsFirst.execution_count - AND qsNow.Pass = 2 - AND (qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - OR qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - OR qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */) - ORDER BY (qsNow.execution_count - COALESCE(qsFirst.execution_count, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - /* Query Stats - CheckID 17 - Most Resource-Intensive Queries */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle) - SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'http://www.BrentOzar.com/go/topqueries', - 'Query stats during the sample:' + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes - (COALESCE(qsFirst.total_logical_writes, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time - (COALESCE(qsFirst.total_clr_time, 0)) AS NVARCHAR(100)) + @LineFeed + - @LineFeed + @LineFeed + 'Query stats since ' + CONVERT(NVARCHAR(100), qsNow.creation_time ,121) + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count AS NVARCHAR(100)) + - CASE qsTotal.execution_count WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time AS NVARCHAR(100)) + - CASE qsTotal.total_elapsed_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time AS NVARCHAR(100)) + - CASE qsTotal.total_worker_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads AS NVARCHAR(100)) + - CASE qsTotal.total_logical_reads WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes AS NVARCHAR(100)) + - CASE qsTotal.total_logical_writes WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_writes / qsTotal.total_logical_writes AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time AS NVARCHAR(100)) + - CASE qsTotal.total_clr_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_clr_time / qsTotal.total_clr_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - --@LineFeed + @LineFeed + 'Query hash: ' + CAST(qsNow.query_hash AS NVARCHAR(100)) + @LineFeed + - --@LineFeed + @LineFeed + 'Query plan hash: ' + CAST(qsNow.query_plan_hash AS NVARCHAR(100)) + - @LineFeed AS Details, - 'See the URL for tuning tips on why this query may be consuming resources.' AS HowToStopIt, - qp.query_plan, - QueryText = SUBSTRING(st.text, - (qsNow.statement_start_offset / 2) + 1, - ((CASE qsNow.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qsNow.statement_end_offset - END - qsNow.statement_start_offset) / 2) + 1), - qsNow.ID AS QueryStatsNowID, - qsFirst.ID AS QueryStatsFirstID, - qsNow.plan_handle AS PlanHandle - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp - WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL - - UPDATE #BlitzFirstResults - SET DatabaseID = CAST(attr.value AS INT), - DatabaseName = DB_NAME(CAST(attr.value AS INT)) - FROM #BlitzFirstResults - CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr - WHERE attr.attribute = 'dbid' - - - END /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ - - - RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; - - /* Wait Stats - CheckID 6 */ - /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT TOP 10 6 AS CheckID, - 200 AS Priority, - 'Wait Stats' AS FindingGroup, - wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ - N'http://www.brentozar.com/sql/wait-stats/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_time_ms > (wBase.wait_time_ms + (.5 * (DATEDIFF(ss,@StartSampleTime,@FinishSampleTime)) * 1000)) /* Only look for things we've actually waited on for half of the time or more */ - ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; - - /* Server Performance - Poison Wait Detected - CheckID 30 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT 30 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Poison Wait Detected: ' + wNow.wait_type AS Finding, - N'http://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') AND wNow.wait_time_ms > wBase.wait_time_ms; - - - /* Server Performance - Slow Data File Reads - CheckID 11 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 11 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Data File Reads' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these reads: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average read latency during the sample: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for data file read speed: 20ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > (fBase.io_stall_read_ms + 1000) - WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'ROWS' - ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; - END - - /* Server Performance - Slow Log File Writes - CheckID 12 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 12 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Log File Writes' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these writes: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average write latency during the sample: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for log file write speed: 3ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > (fBase.io_stall_write_ms + 1000) - WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'LOG' - ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; - END - - - /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 13 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Growing' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-growing/' AS URL, - 'Number of growths 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Growths' - AND value_delta > 0 - - - /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 14 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Shrinking' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-shrinking/' AS URL, - 'Number of shrinks 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Shrinks' - AND value_delta > 0 - - /* Query Problems - Compilations/Sec High - CheckID 15 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 15 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/compilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, - 'To find the queries that are compiling, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'If dynamic SQL or non-parameterized strings are involved, consider enabling Forced Parameterization. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Compilations are more than 10% of batch requests per second */ - - /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 16 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Re-Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/recompilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, - 'To find the queries that are being forced to recompile, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'Examine those plans to find out which objects are changing so quickly that they hit the stats update threshold. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Re-Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Recompilations are more than 10% of batch requests per second */ - - /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 29 AS CheckID, - 40 AS Priority, - 'Table Problems' AS FindingGroup, - 'Forwarded Fetches/Sec High' AS Finding, - 'https://BrentOzar.com/go/fetch/' AS URL, - CAST(ps.value_delta AS NVARCHAR(20)) + ' Forwarded Records (from SQLServer:Access Methods counter)' + @LineFeed - + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, - 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Access Methods' AND psComp.counter_name = 'Forwarded Records/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Access Methods' - AND ps.counter_name = 'Forwarded Records/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - - /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 31 AS CheckID, - 50 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Garbage Collection in Progress' AS Finding, - 'https://BrentOzar.com/go/garbage/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed - + 'This can happen due to memory pressure (causing In-Memory OLTP to shrink its footprint) or' + @LineFeed - + 'due to transactional workloads that constantly insert/delete data.' AS Details, - 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Garbage Collection' - AND ps.counter_name = 'Rows processed/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Transactions Aborted' AS Finding, - 'https://BrentOzar.com/go/aborted/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed - + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, - 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Transactions' AND psComp.counter_name = 'Transactions aborted/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Transactions' - AND ps.counter_name = 'Transactions aborted/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'Query Problems' AS FindingGroup, - 'Suboptimal Plans/Sec High' AS Finding, - 'https://BrentOzar.com/go/suboptimal/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed - + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, - 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Workload GroupStats' AND psComp.counter_name = 'Suboptimal plans/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Workload GroupStats' - AND ps.counter_name = 'Suboptimal plans/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - - - /* Server Info - Batch Requests per Sec - CheckID 19 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Batch Requests per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec'; - - - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - - /* Server Info - SQL Compilations/sec - CheckID 25 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 25 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Compilations/sec'; - - /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 26 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Re-Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Re-Compilations/sec'; - - /* Server Info - Wait Time per Core per Sec - CheckID 20 */ - IF @Seconds > 0 - BEGIN - WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), - waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), - cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Wait Time per Core per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST((waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS NVARCHAR(20)) AS Details, - (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS DetailsInt - FROM cores i - CROSS JOIN waits1 - CROSS JOIN waits2; - END - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds >= 30 - BEGIN - /* If we're waiting 30+ seconds, run this check at the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - END /* IF @Seconds < 30 */ - - - /* If we didn't find anything, apologize. */ - IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) - BEGIN - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 1 , - 'No Problems Found' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' - ); - - END /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - ); - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - VALUES ( -1 , - 0 , - 'sp_BlitzFirst ' + CAST(CONVERT(DATETIMEOFFSET, @VersionDate, 102) AS VARCHAR(100)), - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'We hope you found this tool useful.' - ); - - /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old */ - IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 0 AS Priority , - 'Outdated sp_BlitzFirst' AS FindingsGroup , - 'sp_BlitzFirst is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details - END - - RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; - - - /* If they want to run sp_BlitzCache and export to table, go for it. */ - IF @OutputTableNameBlitzCache IS NOT NULL - AND @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; - - /* Set the sp_BlitzCache sort order based on their top wait type */ - - /* First, check for poison waits - CheckID 30 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 30) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE_QUERY_COMPILE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: THREADPOOL' THEN 'executions' - WHEN Finding = 'Poison Wait Detected: LOG_RATE_GOVERNOR' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_CATCHUP_THROTTLE' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_COMMIT_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_ROLLBACK_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_SLOW_SECONDARY_THROTTLE' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 30 - ORDER BY DetailsInt DESC; - END - - /* Too much free memory - which probably indicates queries finished w/huge grants - CheckID 34 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 34) - SET @BlitzCacheSortOrder = 'memory grant'; - - /* Next, Compilations/Sec High - CheckID 15 and 16 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID IN (15,16)) - SET @BlitzCacheSortOrder = 'compilations'; - - /* Still not set? Use the top wait type. */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 6) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'ASYNC_NETWORK_IO' THEN 'duration' - WHEN Finding = 'CXPACKET' THEN 'reads' - WHEN Finding = 'LATCH_EX' THEN 'reads' - WHEN Finding LIKE 'LCK%' THEN 'duration' - WHEN Finding LIKE 'PAGEIOLATCH%' THEN 'reads' - WHEN Finding = 'SOS_SCHEDULER_YIELD' THEN 'cpu' - WHEN Finding = 'WRITELOG' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 6 - ORDER BY DetailsInt DESC; - END - /* Still null? Just use the default. */ - - - - /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ - IF EXISTS (SELECT * FROM sys.objects o - INNER JOIN sys.parameters pMB ON o.object_id = pMB.object_id AND pMB.name = '@MinutesBack' - INNER JOIN sys.parameters pCDO ON o.object_id = pCDO.object_id AND pCDO.name = '@CheckDateOverride' - WHERE o.name = 'sp_BlitzCache') - BEGIN - /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; - EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; - - /* If there's no data, let's just analyze the last 15 minutes of the plan cache */ - IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 - SET @BlitzCacheMinutesBack = 15; - - IF @BlitzCacheSortOrder IS NOT NULL - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @SortOrder = @BlitzCacheSortOrder, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - ELSE - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - - ELSE /* No sp_BlitzCache found, or it's outdated */ - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 36 AS CheckID , - 0 AS Priority , - 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , - 'Update Your sp_BlitzCache' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details - END - - RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; - - END /* End running sp_BlitzCache */ - - /* @OutputTableName lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND @OutputTableName NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - - EXEC(@StringToExecute); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + (CONVERT(NVARCHAR(100), @StartSampleTime, 121)) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NULL) CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - /* @OutputTableNameFileStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameFileStats IS NOT NULL - AND @OutputTableNameFileStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameFileStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats_View + ' AS ' + @LineFeed - + 'SELECT f.ServerName, f.CheckDate, f.DatabaseID, f.DatabaseName, f.FileID, f.FileLogicalName, f.TypeDesc, f.PhysicalName, f.SizeOnDiskMB' + @LineFeed - + ', DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth' + @LineFeed - + ', (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms' + @LineFeed - + ', io_stall_read_ms_average = CASE WHEN (f.num_of_reads - fPrior.num_of_reads) = 0 THEN 0 ELSE (f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads) END' + @LineFeed - + ', (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads' + @LineFeed - + ', (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read' + @LineFeed - + ', (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms' + @LineFeed - + ', io_stall_write_ms_average = CASE WHEN (f.num_of_writes - fPrior.num_of_writes) = 0 THEN 0 ELSE (f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes) END' + @LineFeed - + ', (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes' + @LineFeed - + ', (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName AND f.DatabaseID = fPrior.DatabaseID AND f.FileID = fPrior.FileID AND f.CheckDate > fPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fMiddle ON f.ServerName = fMiddle.ServerName AND f.DatabaseID = fMiddle.DatabaseID AND f.FileID = fMiddle.FileID AND f.CheckDate > fMiddle.CheckDate AND fMiddle.CheckDate > fPrior.CheckDate' + @LineFeed - + 'WHERE fMiddle.ID IS NULL AND f.num_of_reads >= fPrior.num_of_reads AND f.num_of_writes >= fPrior.num_of_writes - AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameFileStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNamePerfmonStats IS NOT NULL - AND @OutputTableNamePerfmonStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNamePerfmonStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed - + 'SELECT p.ServerName, p.CheckDate, p.object_name, p.counter_name, p.instance_name' + @LineFeed - + ', DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', p.cntr_value' + @LineFeed - + ', p.cntr_type' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) AS cntr_delta' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) * 1.0 / DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS cntr_delta_per_second' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' p' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pPrior ON p.ServerName = pPrior.ServerName AND p.object_name = pPrior.object_name AND p.counter_name = pPrior.counter_name AND p.instance_name = pPrior.instance_name AND p.CheckDate > pPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pMiddle ON p.ServerName = pMiddle.ServerName AND p.object_name = pMiddle.object_name AND p.counter_name = pMiddle.counter_name AND p.instance_name = pMiddle.instance_name AND p.CheckDate > pMiddle.CheckDate AND pMiddle.CheckDate > pPrior.CheckDate' + @LineFeed - + 'WHERE pMiddle.ID IS NULL AND DATEDIFF(MI, pPrior.CheckDate, p.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END; - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNamePerfmonStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNameWaitStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameWaitStats IS NOT NULL - AND @OutputTableNameWaitStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameWaitStats + ''') ' + @LineFeed - + 'BEGIN' + @LineFeed - + 'CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID));' + @LineFeed - + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed - + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed - + 'END' - - EXEC(@StringToExecute); - - /* Create the wait stats category table */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_Categories; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')' - EXEC(@StringToExecute); - END - - /* Make sure the wait stats category table has the current number of rows */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''IF (SELECT COALESCE(SUM(1),0) FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ') <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories)' + @LineFeed - + 'BEGIN ' + @LineFeed - + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed - + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed - + 'END'')' - EXEC(@StringToExecute); - - - /* Create the wait stats view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed - + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed - + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 60000.0 AS wait_time_minutes_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed - + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed - + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND w.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wMiddle ON w.ServerName = wMiddle.ServerName AND w.wait_type = wMiddle.wait_type AND w.CheckDate > wMiddle.CheckDate AND wMiddle.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed - + 'WHERE wMiddle.ID IS NULL AND w.wait_time_ms >= wPrior.wait_time_ms AND DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END - - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameWaitStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - - - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; - - IF @OutputType = 'COUNT' AND @SinceStartup = 0 - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzFirstResults - END - ELSE - IF @OutputType = 'Opserver1' AND @SinceStartup = 0 - BEGIN - - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - r.[Details], - r.[HowToStopIt] , - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; - END - ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 - BEGIN - - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzFirstResults - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - Details; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - [QueryText], - [QueryPlan] - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS NVARCHAR(MAX)) AS Details, - CAST([HowToStopIt] AS NVARCHAR(MAX)) AS HowToStopIt, - CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 1 - BEGIN - IF @SinceStartup = 0 - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - CAST(@StockDetailsHeader + r.[Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + r.HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; - - ------------------------- - --What happened: #WaitStats - ------------------------- - IF @Seconds = 0 - BEGIN - /* Measure waits in hours */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - CAST(c.[Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / 60 / 60 / 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; - END - ELSE - BEGIN - /* Measure waits in seconds */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - c.[Wait Time (Seconds)], - 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 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; - END; - - ------------------------- - --What happened: #FileStats - ------------------------- - WITH readstats AS ( - SELECT 'PHYSICAL READS' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_reads - wd1.num_of_reads ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_reads - wd1.num_of_reads > 0 - THEN CAST(( wd2.bytes_read - wd1.bytes_read)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_read_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ), - writestats AS ( - SELECT - 'PHYSICAL WRITES' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_write_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_writes - wd1.num_of_writes ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_writes - wd1.num_of_writes > 0 - THEN CAST(( wd2.bytes_written - wd1.bytes_written)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_write_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ) - SELECT - Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM readstats - WHERE StallRank <=5 AND [MB Read/Written] > 0 - UNION ALL - SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM writestats - WHERE StallRank <=5 AND [MB Read/Written] > 0; - - - ------------------------- - --What happened: #PerfmonStats - ------------------------- - - SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, - pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, - pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, - pLast.cntr_value - pFirst.cntr_value AS ValueDelta, - ((1.0 * pLast.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pLast.SampleTime)) AS ValuePerSecond - FROM #PerfmonStats pLast - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) - AND pLast.ID > pFirst.ID - WHERE pLast.cntr_value <> pFirst.cntr_value - ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name - - - ------------------------- - --What happened: #QueryStats - ------------------------- - IF @CheckProcedureCache = 1 - BEGIN - - SELECT qsNow.*, qsFirst.* - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.Pass = 2 - END - ELSE - BEGIN - SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details] - END - END - - DROP TABLE #BlitzFirstResults; - - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 -IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - -END /* IF @LogMessage IS NULL */ -END /* ELSE IF @OutputType = 'SCHEMA' */ - -SET NOCOUNT OFF; -GO - - - -/* How to run it: -EXEC dbo.sp_BlitzFirst - -With extra diagnostic info: -EXEC dbo.sp_BlitzFirst @ExpertMode = 1; - -Saving output to tables: -EXEC sp_BlitzFirst -, @OutputDatabaseName = 'DBAtools' -, @OutputSchemaName = 'dbo' -, @OutputTableName = 'BlitzFirst' -, @OutputTableNameFileStats = 'BlitzFirst_FileStats' -, @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' -, @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' -, @OutputTableNameBlitzCache = 'BlitzCache' -*/ -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzIndex AS RETURN 0;') -GO - -ALTER PROCEDURE dbo.sp_BlitzIndex - @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.*/ - @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 */ - /*Note:@Filter doesn't do anything unless @Mode=0*/ - @SkipPartitions BIT = 0, - @SkipStatistics BIT = 1, - @GetAllDatabases BIT = 0, - @BringThePain BIT = 0, - @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @Help TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - - -IF @Help = 1 PRINT ' -/* -sp_BlitzIndex from http://FirstResponderKit.org - -This script analyzes the design and performance of your indexes. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - The @OutputDatabaseName parameters are not functional yet. To check the - status of this enhancement request, visit: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/221 - - Does not analyze columnstore, spatial, XML, or full text indexes. If you - would like to contribute code to analyze those, head over to Github and - check out the issues list: http://FirstResponderKit.org - - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. - -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important - for the user to understand if it is going to be offline and not just run a script. - -- Example 2: they do not include all the options the index may have been created with (padding, compression - filegroup/partition scheme etc.) - -- (The compression and filegroup index create syntax is not trivial because it is set at the partition - level and is not trivial to code.) - - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of insanity.) - -Unknown limitations of this version: - - We knew them once, but we forgot. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/milestone/4?closed=1 - - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -' - - -DECLARE @ScriptVersionName NVARCHAR(50); -DECLARE @DaysUptime NUMERIC(23,2); -DECLARE @DatabaseID INT; -DECLARE @ObjectID INT; -DECLARE @dsql NVARCHAR(MAX); -DECLARE @params NVARCHAR(MAX); -DECLARE @msg NVARCHAR(4000); -DECLARE @ErrorSeverity INT; -DECLARE @ErrorState INT; -DECLARE @Rowcount BIGINT; -DECLARE @SQLServerProductVersion NVARCHAR(128); -DECLARE @SQLServerEdition INT; -DECLARE @FilterMB INT; -DECLARE @collation NVARCHAR(256); -DECLARE @NumDatabases INT; -DECLARE @LineFeed NVARCHAR(5); - -SET @LineFeed = CHAR(13) + CHAR(10); -SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ -SET @FilterMB=250; -SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate) - -RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; - -IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL - DROP TABLE #IndexSanity; - -IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL - DROP TABLE #IndexPartitionSanity; - -IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL - DROP TABLE #IndexSanitySize; - -IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL - DROP TABLE #IndexColumns; - -IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL - DROP TABLE #MissingIndexes; - -IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL - DROP TABLE #ForeignKeys; - -IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL - DROP TABLE #BlitzIndexResults; - -IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL - DROP TABLE #IndexCreateTsql; - -IF OBJECT_ID('tempdb..#DatabaseList') IS NOT NULL - DROP TABLE #DatabaseList; - -IF OBJECT_ID('tempdb..#Statistics') IS NOT NULL - DROP TABLE #Statistics; - -IF OBJECT_ID('tempdb..#PartitionCompressionInfo') IS NOT NULL - DROP TABLE #PartitionCompressionInfo; - -IF OBJECT_ID('tempdb..#ComputedColumns') IS NOT NULL - DROP TABLE #ComputedColumns; - -IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; - -IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL - DROP TABLE #TemporalTables; - - RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; - CREATE TABLE #BlitzIndexResults - ( - blitz_result_id INT IDENTITY PRIMARY KEY, - check_id INT NOT NULL, - index_sanity_id INT NULL, - Priority INT NULL, - findings_group VARCHAR(4000) NOT NULL, - finding VARCHAR(200) NOT NULL, - [database_name] VARCHAR(200) NULL, - URL VARCHAR(200) NOT NULL, - details NVARCHAR(4000) NOT NULL, - index_definition NVARCHAR(MAX) NOT NULL, - secret_columns NVARCHAR(MAX) NULL, - index_usage_summary NVARCHAR(MAX) NULL, - index_size_summary NVARCHAR(MAX) NULL, - create_tsql NVARCHAR(MAX) NULL, - more_info NVARCHAR(MAX)NULL - ); - - CREATE TABLE #IndexSanity - ( - [index_sanity_id] INT IDENTITY PRIMARY KEY CLUSTERED, - [database_id] SMALLINT NOT NULL , - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [index_type] TINYINT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [object_name] NVARCHAR(128) NOT NULL , - index_name NVARCHAR(128) NULL , - key_column_names NVARCHAR(MAX) NULL , - key_column_names_with_sort_order NVARCHAR(MAX) NULL , - key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , - count_key_columns INT NULL , - include_column_names NVARCHAR(MAX) NULL , - include_column_names_no_types NVARCHAR(MAX) NULL , - count_included_columns INT NULL , - partition_key_column_name NVARCHAR(MAX) NULL, - filter_definition NVARCHAR(MAX) NOT NULL , - is_indexed_view BIT NOT NULL , - is_unique BIT NOT NULL , - is_primary_key BIT NOT NULL , - is_XML BIT NOT NULL, - is_spatial BIT NOT NULL, - is_NC_columnstore BIT NOT NULL, - is_CX_columnstore BIT NOT NULL, - is_disabled BIT NOT NULL , - is_hypothetical BIT NOT NULL , - is_padded BIT NOT NULL , - fill_factor SMALLINT NOT NULL , - user_seeks BIGINT NOT NULL , - user_scans BIGINT NOT NULL , - user_lookups BIGINT NOT NULL , - user_updates BIGINT NULL , - last_user_seek DATETIME NULL , - last_user_scan DATETIME NULL , - last_user_lookup DATETIME NULL , - last_user_update DATETIME NULL , - is_referenced_by_foreign_key BIT DEFAULT(0), - secret_columns NVARCHAR(MAX) NULL, - count_secret_columns INT NULL, - create_date DATETIME NOT NULL, - modify_date DATETIME NOT NULL, - [db_schema_object_name] AS [schema_name] + '.' + [object_name] , - [db_schema_object_indexid] AS [schema_name] + '.' + [object_name] - + CASE WHEN [index_name] IS NOT NULL THEN '.' + index_name - ELSE '' - END + ' (' + CAST(index_id AS NVARCHAR(20)) + ')' , - first_key_column_name AS CASE WHEN count_key_columns > 1 - THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) - ELSE key_column_names - END , - index_definition AS - CASE WHEN partition_key_column_name IS NOT NULL - THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' - ELSE '' - END + - CASE index_id - WHEN 0 THEN N'[HEAP] ' - WHEN 1 THEN N'[CX] ' - ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN '[VIEW] ' - ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' - 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_disabled = 1 THEN N'[DISABLED] ' - ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' - ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 THEN N'[UNIQUE] ' - ELSE N'' END + CASE WHEN count_key_columns > 0 THEN - N'[' + CAST(count_key_columns AS VARCHAR(10)) + N' KEY' - + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + LTRIM(key_column_names_with_sort_order) - ELSE N'' END + CASE WHEN count_included_columns > 0 THEN - N' [' + CAST(count_included_columns AS VARCHAR(10)) + N' INCLUDE' + - + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + include_column_names - ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition - ELSE N'' END , - [total_reads] AS user_seeks + user_scans + user_lookups, - [reads_per_write] AS CAST(CASE WHEN user_updates > 0 - THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) - ELSE 0 END AS MONEY) , - [index_usage_summary] AS N'Reads: ' + - REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), '.00', '') - + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN - N' (' - + RTRIM( - CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), '.00', '') + N' seek ' ELSE N'' END - + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), '.00', '') + N' scan ' ELSE N'' END - + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), '.00', '') + N' lookup' ELSE N'' END - ) - + N') ' - ELSE N' ' END - + N'Writes:' + - REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), '.00', ''), - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([object_name],'''') + N';' - ); - RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; - IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') - CREATE UNIQUE INDEX uq_database_id_object_id_index_id ON #IndexSanity (database_id, object_id, index_id); - - - CREATE TABLE #IndexPartitionSanity - ( - [index_partition_sanity_id] INT IDENTITY, - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL , - [object_id] INT NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL, - [index_id] INT NOT NULL , - [partition_number] INT NOT NULL , - row_count BIGINT NOT NULL , - reserved_MB NUMERIC(29,2) NOT NULL , - reserved_LOB_MB NUMERIC(29,2) NOT NULL , - reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - leaf_insert_count BIGINT NULL , - leaf_delete_count BIGINT NULL , - leaf_update_count BIGINT NULL , - range_scan_count BIGINT NULL , - singleton_lookup_count BIGINT NULL , - forwarded_fetch_count BIGINT NULL , - lob_fetch_in_pages BIGINT NULL , - lob_fetch_in_bytes BIGINT NULL , - row_overflow_fetch_in_pages BIGINT NULL , - row_overflow_fetch_in_bytes BIGINT NULL , - row_lock_count BIGINT NULL , - row_lock_wait_count BIGINT NULL , - row_lock_wait_in_ms BIGINT NULL , - page_lock_count BIGINT NULL , - page_lock_wait_count BIGINT NULL , - page_lock_wait_in_ms BIGINT NULL , - index_lock_promotion_attempt_count BIGINT NULL , - index_lock_promotion_count BIGINT NULL, - data_compression_desc VARCHAR(60) NULL - ); - - CREATE TABLE #IndexSanitySize - ( - [index_sanity_size_id] INT IDENTITY NOT NULL , - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128) NOT NULL, - partition_count INT NOT NULL , - total_rows BIGINT NOT NULL , - total_reserved_MB NUMERIC(29,2) NOT NULL , - total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , - total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - total_leaf_delete_count BIGINT NULL, - total_leaf_update_count BIGINT NULL, - total_range_scan_count BIGINT NULL, - total_singleton_lookup_count BIGINT NULL, - total_forwarded_fetch_count BIGINT NULL, - total_row_lock_count BIGINT NULL , - total_row_lock_wait_count BIGINT NULL , - total_row_lock_wait_in_ms BIGINT NULL , - avg_row_lock_wait_in_ms BIGINT NULL , - total_page_lock_count BIGINT NULL , - total_page_lock_wait_count BIGINT NULL , - total_page_lock_wait_in_ms BIGINT NULL , - avg_page_lock_wait_in_ms BIGINT NULL , - total_index_lock_promotion_attempt_count BIGINT NULL , - total_index_lock_promotion_count BIGINT NULL , - data_compression_desc VARCHAR(8000) NULL, - index_size_summary AS ISNULL( - CASE WHEN partition_count > 1 - THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' - ELSE N'' - END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS MONEY), 1), N'.00', N'') + N' rows; ' - + CASE WHEN total_reserved_MB > 1024 THEN - CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' - ELSE - CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' - END - + CASE WHEN total_reserved_LOB_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB LOB' - WHEN total_reserved_LOB_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB LOB' - ELSE '' - END - + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' - WHEN total_reserved_row_overflow_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' - ELSE '' - END , - N'Error- NULL in computed column'), - index_op_stats AS ISNULL( - ( - REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' - + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN - REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' - ELSE N'' END - - /* rows will only be in this dmv when data is in memory for the table */ - ), N'Table metadata not in memory'), - index_lock_wait_summary AS ISNULL( - CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND - total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits.' - ELSE - CASE WHEN total_row_lock_wait_count > 0 THEN - N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_page_lock_wait_count > 0 THEN - N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN - N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') - + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') + N'.' - ELSE N'' - END - END - ,'Error- NULL in computed column') - ); - - CREATE TABLE #IndexColumns - ( - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128), - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [key_ordinal] INT NULL , - is_included_column BIT NULL , - is_descending_key BIT NULL , - [partition_ordinal] INT NULL , - column_name NVARCHAR(256) NOT NULL , - system_type_name NVARCHAR(256) NOT NULL, - max_length SMALLINT NOT NULL, - [precision] TINYINT NOT NULL, - [scale] TINYINT NOT NULL, - collation_name NVARCHAR(256) NULL, - is_nullable BIT NULL, - is_identity BIT NULL, - is_computed BIT NULL, - is_replicated BIT NULL, - is_sparse BIT NULL, - is_filestream BIT NULL, - seed_value BIGINT NULL, - increment_value INT NULL , - last_value BIGINT NULL, - is_not_for_replication BIT NULL - ); - CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns - (database_id, object_id, index_id); - - CREATE TABLE #MissingIndexes - ([database_id] INT NOT NULL, - [object_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [table_name] NVARCHAR(128), - [statement] NVARCHAR(512) NOT NULL, - magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), - avg_total_user_cost NUMERIC(29,4) NOT NULL, - avg_user_impact NUMERIC(29,1) NOT NULL, - user_seeks BIGINT NOT NULL, - user_scans BIGINT NOT NULL, - unique_compiles BIGINT NULL, - equality_columns NVARCHAR(4000), - inequality_columns NVARCHAR(4000), - included_columns NVARCHAR(4000), - is_low BIT, - [index_estimated_impact] AS - REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (user_seeks + user_scans) - AS BIGINT) AS MONEY), 1), '.00', '') + N' use' - + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END - +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) - + N'%; Avg query cost: ' - + CAST(avg_total_user_cost AS NVARCHAR(30)), - [missing_index_details] AS - CASE WHEN equality_columns IS NOT NULL THEN N'EQUALITY: ' + equality_columns + N' ' - ELSE N'' - END + CASE WHEN inequality_columns IS NOT NULL THEN N'INEQUALITY: ' + inequality_columns + N' ' - ELSE N'' - END + CASE WHEN included_columns IS NOT NULL THEN N'INCLUDES: ' + included_columns + N' ' - ELSE N'' - END, - [create_tsql] AS N'CREATE INDEX [ix_' + table_name + N'_' - + REPLACE(REPLACE(REPLACE(REPLACE( - ISNULL(equality_columns,N'')+ - CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END - + ISNULL(inequality_columns,''),',','') - ,'[',''),']',''),' ','_') - + CASE WHEN included_columns IS NOT NULL THEN N'_includes' ELSE N'' END + N'] ON ' - + [statement] + N' (' + ISNULL(equality_columns,N'') - + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END - + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + - ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END - + N' WITH (' - + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - + N';' - , - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';' - ); - - CREATE TABLE #ForeignKeys ( - [database_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - foreign_key_name NVARCHAR(256), - parent_object_id INT, - parent_object_name NVARCHAR(256), - referenced_object_id INT, - referenced_object_name NVARCHAR(256), - is_disabled BIT, - is_not_trusted BIT, - is_not_for_replication BIT, - parent_fk_columns NVARCHAR(MAX), - referenced_fk_columns NVARCHAR(MAX), - update_referential_action_desc NVARCHAR(16), - delete_referential_action_desc NVARCHAR(60) - ) - - CREATE TABLE #IndexCreateTsql ( - index_sanity_id INT NOT NULL, - create_tsql NVARCHAR(MAX) NOT NULL - ) - - CREATE TABLE #DatabaseList ( - DatabaseName NVARCHAR(256), - secondary_role_allow_connections_desc NVARCHAR(50) - - ) - - CREATE TABLE #PartitionCompressionInfo ( - [index_sanity_id] INT NULL, - [partition_compression_detail] VARCHAR(8000) NULL - ) - - CREATE TABLE #Statistics ( - database_id INT NOT NULL, - database_name NVARCHAR(256) NOT NULL, - table_name NVARCHAR(128) NULL, - schema_name NVARCHAR(128) NULL, - index_name NVARCHAR(128) NULL, - column_names NVARCHAR(4000) NULL, - statistics_name NVARCHAR(128) NULL, - last_statistics_update DATETIME NULL, - days_since_last_stats_update INT NULL, - rows BIGINT NULL, - rows_sampled BIGINT NULL, - percent_sampled DECIMAL(18, 1) NULL, - histogram_steps INT NULL, - modification_counter BIGINT NULL, - percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT 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 - ); - - CREATE TABLE #ComputedColumns - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NULL, - database_id INT NOT NULL, - table_name NVARCHAR(128) NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - column_name NVARCHAR(128) NULL, - is_nullable BIT NULL, - definition NVARCHAR(MAX) NULL, - uses_database_collation BIT NOT NULL, - is_persisted BIT NOT NULL, - is_computed BIT NOT NULL, - is_function INT NOT NULL, - column_definition NVARCHAR(MAX) NULL - ); - - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); - - CREATE TABLE #TemporalTables - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NOT NULL, - database_id INT NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - table_name NVARCHAR(128) NOT NULL, - history_table_name NVARCHAR(128) 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 - ); - -/* Sanitize our inputs */ -SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName) - - -IF @GetAllDatabases = 1 - BEGIN - INSERT INTO #DatabaseList (DatabaseName) - SELECT DB_NAME(database_id) - FROM sys.databases - WHERE user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE' - AND database_id > 4 - AND DB_NAME(database_id) NOT LIKE 'ReportServer%' - AND is_distributor = 0; - - /* Skip non-readable databases in an AG - see Github issue #1160 */ - IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') - BEGIN - SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( - SELECT d.name - FROM sys.dm_hadr_availability_replica_states rs - INNER JOIN sys.databases d ON rs.replica_id = d.replica_id - INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id - WHERE rs.role_desc = ''SECONDARY'' - AND r.secondary_role_allow_connections_desc = ''NO'');' - EXEC sp_executesql @dsql; - - IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'Skipped non-readable AG secondary databases.', - N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', - N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', - 'http://FirstResponderKit.org', '', '', '', '' - ); - END - END - - END -ELSE - BEGIN - INSERT INTO #DatabaseList - ( DatabaseName ) - SELECT CASE WHEN @DatabaseName IS NULL OR @DatabaseName = N'' THEN DB_NAME() - ELSE @DatabaseName END - END - -SET @NumDatabases = @@ROWCOUNT; - -/* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ - -BEGIN TRY - IF @NumDatabases >= 50 AND @BringThePain != 1 - BEGIN - - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', - N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary insanity for the server and/or user.', - N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', - 'http://FirstResponderKit.org', '', '', '', '' - ); - - - SELECT bir.blitz_result_id, - bir.check_id, - bir.index_sanity_id, - bir.Priority, - bir.findings_group, - bir.finding, - bir.database_name, - bir.URL, - bir.details, - bir.index_definition, - bir.secret_columns, - bir.index_usage_summary, - bir.index_size_summary, - bir.create_tsql, - bir.more_info - FROM #BlitzIndexResults AS bir - - RETURN; - - END -END TRY -BEGIN CATCH - RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; - - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; - END CATCH; - -/* Permission granted or unnecessary? Ok, let's go! */ - -DECLARE c1 CURSOR -LOCAL FAST_FORWARD -FOR -SELECT DatabaseName FROM #DatabaseList WHERE COALESCE(secondary_role_allow_connections_desc, 'OK') <> 'NO' ORDER BY DatabaseName - -OPEN c1 -FETCH NEXT FROM c1 INTO @DatabaseName - WHILE @@FETCH_STATUS = 0 -BEGIN - - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@DatabaseName, 0, 1) WITH NOWAIT; - -SELECT @DatabaseID = [database_id] -FROM sys.databases - WHERE [name] = @DatabaseName - AND user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE'; - -/* Last startup */ -SELECT @DaysUptime = CAST(DATEDIFF(hh,create_date,GETDATE())/24. AS NUMERIC (23,2)) -FROM sys.databases -WHERE database_id = 2; - -IF @DaysUptime = 0 SET @DaysUptime = .01; - ----------------------------------------- ---STEP 1: OBSERVE THE PATIENT ---This step puts index information into temp tables. ----------------------------------------- -BEGIN TRY - BEGIN - - --Validate SQL Server Verson - - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 - )) <= 9 - BEGIN - SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2008 and higher. The version of this instance is: ' + @SQLServerProductVersion; - RAISERROR(@msg,16,1); - END - - --Short circuit here if database name does not exist. - IF @DatabaseName IS NULL OR @DatabaseID IS NULL - BEGIN - SET @msg='Database does not exist or is not online/multi-user: cannot proceed.' - RAISERROR(@msg,16,1); - END - - --Validate parameters. - IF (@Mode NOT IN (0,1,2,3,4)) - BEGIN - SET @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail, 4=diagnose detail'; - RAISERROR(@msg,16,1); - END - - IF (@Mode <> 0 AND @TableName IS NOT NULL) - BEGIN - SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; - RAISERROR(@msg,16,1); - END - - IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) - BEGIN - SET @msg=N'@Filter only appies when @Mode=0 and @TableName is not specified. Please try again.'; - RAISERROR(@msg,16,1); - END - - IF (@SchemaName IS NOT NULL AND @TableName IS NULL) - BEGIN - SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.' - RAISERROR(@msg,16,1); - END - - - IF (@TableName IS NOT NULL AND @SchemaName IS NULL) - BEGIN - SET @SchemaName=N'dbo' - SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.' - RAISERROR(@msg,1,1) WITH NOWAIT; - END - - --If a table is specified, grab the object id. - --Short circuit if it doesn't exist. - IF @TableName IS NOT NULL - BEGIN - SET @dsql = N' - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @ObjectID= OBJECT_ID - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on - so.schema_id=sc.schema_id - where so.type in (''U'', ''V'') - and so.name=' + QUOTENAME(@TableName,'''')+ N' - and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' - /*Has a row in sys.indexes. This lets us get indexed views.*/ - and exists ( - SELECT si.name - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si - WHERE so.object_id=si.object_id) - OPTION (RECOMPILE);'; - - SET @params='@ObjectID INT OUTPUT' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; - - IF @ObjectID IS NULL - BEGIN - SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + - N'Please check your parameters.' - RAISERROR(@msg,1,1); - RETURN; - END - END - - --set @collation - SELECT @collation=collation_name - FROM sys.databases - WHERE database_id=@DatabaseID; - - --insert columns for clustered indexes and heaps - --collect info on identity columns for this one - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', - CAST(ic.seed_value AS BIGINT), - CAST(ic.increment_value AS INT), - CAST(ic.last_value AS BIGINT), - ic.is_not_for_replication - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON - si.object_id=c.object_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON - c.object_id=ic.object_id and - c.column_id=ic.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) - EXEC sp_executesql @dsql; - - --insert columns for nonclustered indexes - --this uses a full join to sys.index_columns - --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL AS is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL AS is_filestream' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON - si.object_id=c.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id not in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream ) - EXEC sp_executesql @dsql; - - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - so.object_id, - si.index_id, - si.type, - ' + QUOTENAME(@DatabaseName, '''') + ' AS database_name, - COALESCE(sc.NAME, ''Unknown'') AS [schema_name], - COALESCE(so.name, ''Unknown'') AS [object_name], - COALESCE(si.name, ''Unknown'') AS [index_name], - CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, - si.is_unique, - si.is_primary_key, - CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, - 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, - si.is_disabled, - si.is_hypothetical, - si.is_padded, - si.fill_factor,' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN ' - CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition - ELSE '''' - END AS filter_definition' ELSE ''''' AS filter_definition' END + ' - , ISNULL(us.user_seeks, 0), ISNULL(us.user_scans, 0), - ISNULL(us.user_lookups, 0), ISNULL(us.user_updates, 0), us.last_user_seek, us.last_user_scan, - us.last_user_lookup, us.last_user_update, - so.create_date, so.modify_date - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si WITH (NOLOCK) - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas sc ON so.schema_id = sc.schema_id - 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)) + ' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + - CASE WHEN @TableName IS NOT NULL THEN ' and so.name=' + QUOTENAME(@TableName,'''') + ' ' ELSE '' END + - 'OPTION ( RECOMPILE ); - '; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; - 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_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, - is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, user_seeks, user_scans, - user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, - create_date, modify_date ) - EXEC sp_executesql @dsql; - - - RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; - IF @BringThePain = 0 AND @SkipPartitions = 0 - BEGIN - /* Count the total number of partitions */ - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; - EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; - IF @Rowcount > 100 - BEGIN - RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',16,1) WITH NOWAIT; - SET @SkipPartitions = 1; - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'Some Checks Were Skipped', - '@SkipPartitions Forced to 1', - 'http://FirstResponderKit.org', CAST(@Rowcount AS VARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' - ); - END - END - - - - IF (@SkipPartitions = 0) - BEGIN - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 )) <= 2147483647 --Make change here - BEGIN - - RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - - --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 - --This change was made because on a table with lots of paritions, the OUTER APPLY was crazy slow. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN 'par.data_compression_desc ' ELSE 'null as data_compression_desc' END + ' - FROM ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + ', NULL, NULL,NULL) AS os ON - ps.object_id=os.object_id and ps.index_id=os.index_id and ps.partition_number=os.partition_number - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + ' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END - ELSE - BEGIN - RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. - --If you have a lot of paritions and this suddenly starts running for a long time, change it back. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; - INSERT #IndexPartitionSanity ( [database_id], - [object_id], - [schema_name], - index_id, - partition_number, - row_count, - reserved_MB, - reserved_LOB_MB, - reserved_row_overflow_MB, - leaf_insert_count, - leaf_delete_count, - leaf_update_count, - range_scan_count, - singleton_lookup_count, - forwarded_fetch_count, - lob_fetch_in_pages, - lob_fetch_in_bytes, - row_overflow_fetch_in_pages, - row_overflow_fetch_in_bytes, - row_lock_count, - row_lock_wait_count, - row_lock_wait_in_ms, - page_lock_count, - page_lock_wait_count, - page_lock_wait_in_ms, - index_lock_promotion_attempt_count, - index_lock_promotion_count, - data_compression_desc ) - EXEC sp_executesql @dsql; - - END; --End Check For @SkipPartitions = 0 - - - - RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT id.database_id, id.object_id, ' + QUOTENAME(@DatabaseName,'''') + N', sc.[name], so.[name], id.statement , gs.avg_total_user_cost, - gs.avg_user_impact, gs.user_seeks, gs.user_scans, gs.unique_compiles,id.equality_columns, - id.inequality_columns,id.included_columns - FROM sys.dm_db_missing_index_groups ig - JOIN sys.dm_db_missing_index_details id ON ig.index_handle = id.index_handle - JOIN sys.dm_db_missing_index_group_stats gs ON ig.index_group_handle = gs.group_handle - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so on - id.object_id=so.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc on - so.schema_id=sc.schema_id - WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + ' - ' + CASE WHEN @ObjectID IS NULL THEN N'' - ELSE N'and id.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - END + - N'OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, - avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, - inequality_columns, included_columns) - EXEC sp_executesql @dsql; - - SET @dsql = N' - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' 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 - 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(''.'', ''varchar(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(''.'', ''varchar(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); - - RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; - INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, - is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, - [update_referential_action_desc], [delete_referential_action_desc] ) - EXEC sp_executesql @dsql; - - - IF @SkipStatistics = 0 - BEGIN - IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) - OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) - OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) - BEGIN - RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, ddsp.last_updated) AS last_statistics_update, - DATEDIFF(DAY, ddsp.last_updated, GETDATE()) AS days_since_last_stats_update, - ddsp.rows, - ddsp.rows_sampled, - CAST(ddsp.rows_sampled / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) AS percent_sampled, - ddsp.steps AS histogram_steps, - ddsp.modification_counter, - CASE WHEN ddsp.modification_counter > 0 - THEN CAST(ddsp.modification_counter / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE ddsp.modification_counter - END AS percent_modifications, - CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) - 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 - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END - ELSE - BEGIN - RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, STATS_DATE(s.object_id, s.stats_id)) AS last_statistics_update, - DATEDIFF(DAY, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_since_last_stats_update, - si.rowcnt, - si.rowmodctr, - CASE WHEN si.rowmodctr > 0 THEN CAST(si.rowmodctr / ( 1. * NULLIF(si.rowcnt, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE si.rowmodctr - END AS percent_modifications, - CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) - 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, - ' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' - THEN N's.has_filter, - s.filter_definition' - ELSE N'NULL AS has_filter, - NULL AS filter_definition' END - + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si - ON si.name = s.name - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - AND si.rowcnt > 0 - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END - - END - - IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) - BEGIN - RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - t.name AS table_name, - s.name AS schema_name, - c.name AS column_name, - cc.is_nullable, - cc.definition, - cc.uses_database_collation, - cc.is_persisted, - cc.is_computed, - CASE WHEN cc.definition LIKE ''%.%'' THEN 1 ELSE 0 END AS is_function, - ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + - '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + - CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.computed_columns AS cc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON cc.object_id = c.object_id - AND cc.column_id = c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON t.object_id = cc.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON s.schema_id = t.schema_id - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - INSERT #ComputedColumns - ( [database_name], database_id, table_name, schema_name, column_name, is_nullable, definition, - uses_database_collation, is_persisted, is_computed, is_function, column_definition ) - EXEC sp_executesql @dsql; - - END - - RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; - INSERT #TraceStatus - EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS') - - IF (PARSENAME(@SQLServerProductVersion, 4) >= 13) - BEGIN - RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - s.name AS schema_name, - t.name AS table_name, - oa.hsn as history_schema_name, - oa.htn AS history_table_name, - c1.name AS start_column_name, - c2.name AS end_column_name, - p.name AS period_name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.periods AS p - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON p.object_id = t.object_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c1 - ON t.object_id = c1.object_id - AND p.start_column_id = c1.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c2 - ON t.object_id = c2.object_id - AND p.end_column_id = c2.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON t.schema_id = s.schema_id - CROSS APPLY ( SELECT s2.name as hsn, t2.name htn - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t2 - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s2 - ON t2.schema_id = s2.schema_id - WHERE t2.object_id = t.history_table_id - AND t2.temporal_type = 1 /*History table*/ ) AS oa - WHERE t.temporal_type IN ( 2, 4 ) /*BOL currently points to these types, but has no definition for 4*/ - OPTION (RECOMPILE); - ' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_table_name, - history_schema_name, start_column_name, end_column_name, period_name ) - - EXEC sp_executesql @dsql; - - END - -END -END TRY -BEGIN CATCH - RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; - - IF @dsql IS NOT NULL - BEGIN - SET @msg= 'Last @dsql: ' + @dsql; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; - - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; -END CATCH; - FETCH NEXT FROM c1 INTO @DatabaseName -END -DEALLOCATE c1; - - - - - - ----------------------------------------- ---STEP 2: PREP THE TEMP TABLES ---EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. ----------------------------------------- - -RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names = D1.key_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') ,TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D1 ( key_column_names ) - -RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET partition_key_column_name = D1.partition_key_column_name -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1,''))) D1 - ( partition_key_column_name ) - -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order ) - -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order_no_types ) - -RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names = D3.include_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names ); - -RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names_no_types = D3.include_column_names_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names_no_types ); - -RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET count_included_columns = D4.count_included_columns, - count_key_columns = D4.count_key_columns -FROM #IndexSanity si - CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 - ELSE 0 - END) AS count_included_columns, - SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 - ELSE 0 - END) AS count_key_columns - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - ) AS D4 ( count_included_columns, count_key_columns ); - -RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; -UPDATE #IndexPartitionSanity -SET index_sanity_id = i.index_sanity_id -FROM #IndexPartitionSanity ps - JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] - AND ps.index_id = i.index_id - AND i.database_id = ps.database_id - AND i.schema_name = ps.schema_name - - -RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; -INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], partition_count, total_rows, total_reserved_MB, - total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_range_scan_count, - total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, - total_forwarded_fetch_count,total_row_lock_count, - total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, - total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, - avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, - total_index_lock_promotion_count, data_compression_desc ) - SELECT index_sanity_id, ipp.database_id, ipp.schema_name, - COUNT(*), SUM(row_count), SUM(reserved_MB), SUM(reserved_LOB_MB), - SUM(reserved_row_overflow_MB), - SUM(range_scan_count), - SUM(singleton_lookup_count), - SUM(leaf_delete_count), - SUM(leaf_update_count), - SUM(forwarded_fetch_count), - SUM(row_lock_count), - SUM(row_lock_wait_count), - SUM(row_lock_wait_in_ms), - CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN - SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) - ELSE 0 END AS avg_row_lock_wait_in_ms, - SUM(page_lock_count), - SUM(page_lock_wait_count), - SUM(page_lock_wait_in_ms), - CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN - SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) - ELSE 0 END AS avg_page_lock_wait_in_ms, - SUM(index_lock_promotion_attempt_count), - SUM(index_lock_promotion_count), - LEFT(MAX(data_compression_info.data_compression_rollup),8000) - FROM #IndexPartitionSanity ipp - /* individual partitions can have distinct compression settings, just roll them into a list here*/ - OUTER APPLY (SELECT STUFF(( - SELECT N', ' + data_compression_desc - FROM #IndexPartitionSanity ipp2 - WHERE ipp.[object_id]=ipp2.[object_id] - AND ipp.[index_id]=ipp2.[index_id] - AND ipp.database_id = ipp2.database_id - AND ipp.schema_name = ipp2.schema_name - ORDER BY ipp2.partition_number - FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '')) - data_compression_info(data_compression_rollup) - GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name - ORDER BY index_sanity_id -OPTION ( RECOMPILE ); - -RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; -UPDATE #MissingIndexes -SET is_low = CASE WHEN (user_seeks + user_scans) < 10000 - OR avg_user_impact < 70. THEN 1 - ELSE 0 - END; - -RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; -UPDATE #IndexSanity - SET is_referenced_by_foreign_key=1 -FROM #IndexSanity s -JOIN #ForeignKeys fk ON - s.object_id=fk.referenced_object_id - AND s.database_id=fk.database_id - AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns - -RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; -UPDATE nc -SET secret_columns= - N'[' + - CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS VARCHAR(10)) END + - CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + - CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + - CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + - /* Uniquifiers only needed on non-unique clustereds-- not heaps */ - CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END - END - , count_secret_columns= - CASE tb.index_id WHEN 0 THEN 1 ELSE - tb.count_key_columns + - CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END - END -FROM #IndexSanity AS nc -JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id - AND nc.database_id = tb.database_id - AND nc.schema_name = tb.schema_name - AND tb.index_id IN (0,1) -WHERE nc.index_id > 1; - -RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; -UPDATE tb -SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END - , count_secret_columns = 1 -FROM #IndexSanity AS tb -WHERE tb.index_id = 0 /*Heaps-- these have the RID */ - OR (tb.index_id=1 AND tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ - - -RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; -INSERT #IndexCreateTsql (index_sanity_id, create_tsql) -SELECT - index_sanity_id, - ISNULL ( - /* Script drops for disabled non-clustered indexes*/ - CASE WHEN is_disabled = 1 AND index_id <> 1 - THEN N'--DROP INDEX ' + QUOTENAME([index_name]) + N' ON ' - + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) - ELSE - CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' - ELSE - CASE WHEN is_XML = 1 OR is_spatial=1 THEN N'' /* Not even trying for these just yet...*/ - ELSE - CASE WHEN is_primary_key=1 THEN - N'ALTER TABLE ' + QUOTENAME([schema_name]) + - N'.' + QUOTENAME([object_name]) + - N' ADD CONSTRAINT [' + - index_name + - N'] PRIMARY KEY ' + - CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + - key_column_names_with_sort_order_no_types + N' )' - WHEN is_CX_columnstore= 1 THEN - N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) - ELSE /*Else not a PK or cx columnstore */ - N'CREATE ' + - CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + - CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + - CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' - ELSE N'' END + - N'INDEX [' - + index_name + N'] ON ' + - QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + - CASE WHEN is_NC_columnstore=1 THEN - N' (' + ISNULL(include_column_names_no_types,'') + N' )' - ELSE /*Else not colunnstore */ - N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' - + CASE WHEN include_column_names_no_types IS NOT NULL THEN - N' INCLUDE (' + include_column_names_no_types + N')' - ELSE N'' - END - END /*End non-colunnstore case */ - + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END - END /*End Non-PK index CASE */ - + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN - N' WITH (' - + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' - + N'ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - ELSE N'' END - + N';' - END /*End non-spatial and non-xml CASE */ - END - END, '[Unknown Error]') - AS create_tsql -FROM #IndexSanity; - -RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; -;WITH [maps] - AS ( SELECT - index_sanity_id, - partition_number, - data_compression_desc, - partition_number - ROW_NUMBER() OVER (PARTITION BY ips.index_sanity_id, data_compression_desc ORDER BY partition_number ) AS [rN] - FROM #IndexPartitionSanity ips - ), - [grps] - AS ( SELECT MIN([maps].[partition_number]) AS [MinKey] , - MAX([maps].[partition_number]) AS [MaxKey] , - index_sanity_id, - maps.data_compression_desc - FROM [maps] - GROUP BY [maps].[rN], index_sanity_id, maps.data_compression_desc) -INSERT #PartitionCompressionInfo - (index_sanity_id, partition_compression_detail) -SELECT DISTINCT grps.index_sanity_id , SUBSTRING(( STUFF((SELECT ', ' + ' Partition' - + CASE WHEN [grps2].[MinKey] < [grps2].[MaxKey] - THEN +'s ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' - ' - + CAST([grps2].[MaxKey] AS VARCHAR) - + ' use ' + grps2.data_compression_desc - ELSE ' ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' uses ' + grps2.data_compression_desc - END AS [Partitions] - FROM [grps] AS grps2 - WHERE grps2.index_sanity_id = grps.index_sanity_id - ORDER BY grps2.MinKey, grps2.MaxKey - FOR XML PATH('') , - TYPE - ).[value]('.', 'VARCHAR(MAX)'), 1, 1, '') ), 0, 8000) AS [partition_compression_detail] -FROM grps; - -RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; -UPDATE sz -SET sz.data_compression_desc = pci.partition_compression_detail -FROM #IndexSanitySize sz -JOIN #PartitionCompressionInfo AS pci -ON pci.index_sanity_id = sz.index_sanity_id; - - - -/*This is for debugging*/ ---SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; ---SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; ---SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; ---SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; ---SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; ---SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; ---SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults; ---SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; ---SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; ---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; -/*End debug*/ - - ----------------------------------------- ---STEP 3: DIAGNOSE THE PATIENT ----------------------------------------- - - -BEGIN TRY ----------------------------------------- ---If @TableName is specified, just return information for that table. ---The @Mode parameter doesn't matter if you're looking at a specific table. ----------------------------------------- -IF @TableName IS NOT NULL -BEGIN - RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; - - --We do a left join here in case this is a disabled NC. - --In that case, it won't have any size info/pages allocated. - - - WITH table_mode_cte AS ( - SELECT - s.db_schema_object_indexid, - s.key_column_names, - s.index_definition, - ISNULL(s.secret_columns,N'') AS secret_columns, - s.fill_factor, - s.index_usage_summary, - sz.index_op_stats, - ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, - partition_compression_detail , - ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, - s.is_referenced_by_foreign_key, - (SELECT COUNT(*) - FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id - AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, - s.last_user_seek, - s.last_user_scan, - s.last_user_lookup, - s.last_user_update, - s.create_date, - s.modify_date, - ct.create_tsql, - 1 AS display_order - FROM #IndexSanity s - LEFT JOIN #IndexSanitySize sz ON - s.index_sanity_id=sz.index_sanity_id - LEFT JOIN #IndexCreateTsql ct ON - s.index_sanity_id=ct.index_sanity_id - LEFT JOIN #PartitionCompressionInfo pci ON - pci.index_sanity_id = s.index_sanity_id - WHERE s.[object_id]=@ObjectID - UNION ALL - SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + - N' (' + @ScriptVersionName + ')' , - N'SQL Server First Responder Kit' , - N'http://FirstResponderKit.org' , - N'From Your Community Volunteers', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - 0 AS display_order - ) - SELECT - db_schema_object_indexid AS [Details: db_schema.table.index(indexid)], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - secret_columns AS [Secret Columns], - fill_factor AS [Fillfactor], - index_usage_summary AS [Usage Stats], - index_op_stats AS [Op Stats], - index_size_summary AS [Size], - partition_compression_detail AS [Compression Type], - index_lock_wait_summary AS [Lock Waits], - is_referenced_by_foreign_key AS [Referenced by FK?], - FKs_covered_by_index AS [FK Covered by Index?], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Write], - create_date AS [Created], - modify_date AS [Last Modified], - create_tsql AS [Create TSQL] - FROM table_mode_cte - ORDER BY display_order ASC, key_column_names ASC - OPTION ( RECOMPILE ); - - IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL - BEGIN - - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT N'Missing index.' AS Finding , - N'http://BrentOzar.com/go/Indexaphobia' AS URL , - mi.[statement] + - ' Est. Benefit: ' - + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS [Estimated Benefit], - missing_index_details AS [Missing Index Request] , - index_estimated_impact AS [Estimated Impact], - create_tsql AS [Create TSQL] - FROM #MissingIndexes mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - WHERE mi.[object_id] = @ObjectID - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - AND (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - ORDER BY is_low, magic_benefit_number DESC - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No missing indexes.' AS finding; - - SELECT - column_name AS [Column Name], - (SELECT COUNT(*) - FROM #IndexColumns c2 - WHERE c2.column_name=c.column_name - AND c2.key_ordinal IS NOT NULL) - + CASE WHEN c.index_id = 1 AND c.key_ordinal IS NOT NULL THEN - -1+ (SELECT COUNT(DISTINCT index_id) - FROM #IndexColumns c3 - WHERE c3.index_id NOT IN (0,1)) - ELSE 0 END - AS [Found In], - system_type_name + - CASE max_length WHEN -1 THEN N' (max)' ELSE - CASE - WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' - WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' - ELSE '' - END - END - AS [Type], - CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], - max_length AS [Length (max bytes)], - [precision] AS [Prec], - [scale] AS [Scale], - CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], - CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], - CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], - CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], - CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], - collation_name AS [Collation] - FROM #IndexColumns AS c - WHERE index_id IN (0,1); - - IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL - BEGIN - SELECT [database_name] + N':' + parent_object_name + N': ' + foreign_key_name AS [Foreign Key], - parent_fk_columns AS [Foreign Key Columns], - referenced_object_name AS [Referenced Table], - referenced_fk_columns AS [Referenced Table Columns], - is_disabled AS [Is Disabled?], - is_not_trusted AS [Not Trusted?], - is_not_for_replication [Not for Replication?], - [update_referential_action_desc] AS [Cascading Updates?], - [delete_referential_action_desc] AS [Cascading Deletes?] - FROM #ForeignKeys - ORDER BY [Foreign Key] - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No foreign keys.' AS finding; -END - ---If @TableName is NOT specified... ---Act based on the @Mode and @Filter. (@Filter applies only when @Mode=0 "diagnose") -ELSE -BEGIN; - IF @Mode IN (0, 4) /* DIAGNOSE*/ - BEGIN; - RAISERROR(N'@Mode=0 or 4, we are diagnosing.', 0,1) WITH NOWAIT; - - ---------------------------------------- - --Multiple Index Personalities: Check_id 0-10 - ---------------------------------------- - BEGIN; - - --SELECT [object_id], key_column_names, database_id - -- FROM #IndexSanity - -- WHERE index_type IN (1,2) /* Clustered, NC only*/ - -- AND is_hypothetical = 0 - -- AND is_disabled = 0 - -- GROUP BY [object_id], key_column_names, database_id - -- HAVING COUNT(*) > 1 - - - RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; - WITH duplicate_indexes - AS ( SELECT [object_id], key_column_names, database_id, [schema_name] - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical = 0 - AND is_disabled = 0 - AND is_primary_key = 0 - GROUP BY [object_id], key_column_names, database_id, [schema_name] - HAVING COUNT(*) > 1) - 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 1 AS check_id, - ip.index_sanity_id, - 50 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Duplicate keys' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM duplicate_indexes di - JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] - AND ip.database_id = di.database_id - AND ip.[schema_name] = di.[schema_name] - AND di.key_column_names = ip.key_column_names - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id AND ip.database_id = ips.database_id - /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ - WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END - AND ip.is_primary_key = 0 - ORDER BY ip.object_id, ip.key_column_names_with_sort_order - OPTION ( RECOMPILE ); - - RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; - WITH borderline_duplicate_indexes - AS ( SELECT DISTINCT database_id, [object_id], first_key_column_name, key_column_names, - COUNT([object_id]) OVER ( PARTITION BY database_id, [object_id], first_key_column_name ) AS number_dupes - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical=0 - AND is_disabled=0 - AND is_primary_key = 0) - 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 2 AS check_id, - ip.index_sanity_id, - 60 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Borderline duplicate keys' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - ip.db_schema_object_indexid AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM #IndexSanity AS ip - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id - WHERE EXISTS ( - SELECT di.[object_id] - FROM borderline_duplicate_indexes AS di - WHERE di.[object_id] = ip.[object_id] AND - di.database_id = ip.database_id AND - di.first_key_column_name = ip.first_key_column_name AND - di.key_column_names <> ip.key_column_names AND - di.number_dupes > 1 - ) - AND ip.is_primary_key = 0 - /* WHERE clause skips near-duplicate indexes when getting all databases or using PainRelief mode */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - ORDER BY ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names - OPTION ( RECOMPILE ); - - END - ---------------------------------------- - --Aggressive Indexes: Check_id 10-19 - ---------------------------------------- - BEGIN; - - RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page) with long average waits', 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 11 AS check_id, - i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with long average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) > 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 12: Total lock wait time > 5 minutes (row + page) with short average waits', 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 12 AS check_id, - i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with short average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) < 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id - OPTION ( RECOMPILE ); - - END - - ---------------------------------------- - --Index Hoarder: Check_id 20-29 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 20: >=7 NC indexes on any given table. Yes, 7 is an arbitrary number.', 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 20 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 100 AS Priority, - 'Index Hoarder' AS findings_group, - 'Many NC indexes on a single table' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, - i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, - '' AS secret_columns, - REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS MONEY), 1), N'.00', N'') + N' reads (ALL); ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS MONEY), 1), N'.00', N'') + N' writes (ALL); ', - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS MONEY), 1), N'.00', N'') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - GROUP BY db_schema_object_name, [i].[database_name] - HAVING COUNT(*) >= 7 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - IF @Filter = 1 /*@Filter=1 is "ignore unusued" */ - BEGIN - RAISERROR(N'Skipping checks on unused indexes (21 and 22) because @Filter=1', 0,1) WITH NOWAIT; - END - ELSE /*Otherwise, go ahead and do the checks*/ - BEGIN - RAISERROR(N'check_id 21: >=5 percent of indexes are unused. Yes, 5 is an arbitrary number.', 0,1) WITH NOWAIT; - DECLARE @percent_NC_indexes_unused NUMERIC(29,1); - DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); - - SELECT @percent_NC_indexes_unused =( 100.00 * SUM(CASE WHEN total_reads = 0 THEN 1 - ELSE 0 - END) ) / COUNT(*) , - @NC_indexes_unused_reserved_MB = SUM(CASE WHEN total_reads = 0 THEN sz.total_reserved_MB - ELSE 0 - END) - FROM #IndexSanity i - 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()) - 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, - secret_columns, index_usage_summary, index_size_summary ) - SELECT 21 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'More than 5 percent NC indexes are unused' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + - N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, - i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, - '' AS secret_columns, - CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' - + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, - - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND i.is_unique = 0 - AND total_reads = 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 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()) - GROUP BY i.database_name - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 22: NC indexes with 0 reads. (Borderline) and >= 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with High Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates >= 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); - END /*end checks only run when @Filter <> 1*/ - - RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Borderline: Wide indexes (7 or more columns)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' - + i.db_schema_object_indexid AS details, i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE ( count_key_columns + count_included_columns ) >= 7 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT database_id, [object_id], - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND key_ordinal > 0 - GROUP BY database_id, object_id - ) - 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 24 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide clustered index (> 3 columns OR > 16 bytes)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' - + CAST(cc.sum_max_length AS NVARCHAR(10)) - + N' bytes in clustered index:' + i.db_schema_object_name - + N'. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, - i.index_usage_summary, - ip.index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND i.database_id = cc.database_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (count_key_columns > 3 /*More than three key columns.*/ - OR cc.sum_max_length > 16 /*More than 16 bytes in key */) - AND i.is_CX_columnstore = 0 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 25: Addicted to nullable columns.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) AS non_nullable_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 25 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to nulls' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = ip.database_id - AND cc.[schema_name] = ip.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND cc.non_nullable_columns < 2 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE max_length WHEN -1 THEN 1 ELSE 0 END) AS count_lob_columns, - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 26 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide tables: 35+ cols or > 2000 non-LOB bytes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' has ' + CAST((total_columns) AS NVARCHAR(10)) - + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) - + N' bytes.' + - CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) AS NVARCHAR(10)) - + ' columns are LOB types.' ELSE '' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (cc.total_columns >= 35 OR - cc.sum_max_length >= 2000) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 27: Addicted to strings.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE WHEN system_type_name IN ('varchar','nvarchar','char') OR max_length=-1 THEN 1 ELSE 0 END) AS string_or_LOB_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 27 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to strings' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns. Check if data types are valid.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND calc1.non_string_or_lob_columns <= 1 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Non-Unique clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name - + N' and all NC indexes. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, - i.index_usage_summary, - ip.index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND is_unique=0 /* not unique */ - AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ) - - RAISERROR(N'check_id 29: NC indexes with 0 reads. (Borderline) and < 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with Low Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates < 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); - - END - ---------------------------------------- - --Feature-Phobic Indexes: Check_id 30-39 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; - /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 - */ - - SELECT database_name, - SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, - 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes - INTO #index_includes - FROM #IndexSanity - GROUP BY database_name; - - IF NOT (@Mode = 0) - 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 30 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - database_name AS [Database Name], - N'No indexes use includes' AS finding, 'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'No indexes use includes' AS details, - database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes = 0 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 0,1) WITH NOWAIT; - IF NOT (@Mode = 0) - 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 31 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: Includes are used in < 3% of indexes' AS findings, - database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, - N'Entire database' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 32: filtered indexes and indexed views', 0,1) WITH NOWAIT; - - IF NOT (@Mode = 0) - 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 DISTINCT - 32 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: No filtered indexes or indexed views exist' AS finding, - i.database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'These are NOT always needed-- but do you know when you would use them?' AS details, - i.database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #IndexSanity i - WHERE i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE filter_definition <> '' ) - AND i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE is_indexed_view = 1 ) - OPTION ( RECOMPILE ); - END; - - RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, - i.index_sanity_id AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Potential filtered index (based on column name)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexColumns ic - JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] - AND ic.database_id =i.database_id - AND ic.schema_name = i.schema_name - AND ic.[index_id]=i.[index_id] - AND i.[index_id] > 1 /* non-clustered index */ - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (column_name LIKE 'is%' - OR column_name LIKE '%archive%' - OR column_name LIKE '%active%' - OR column_name LIKE '%flag%') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - ---------------------------------------- - --Self Loathing Indexes : Check_id 40-49 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: nonclustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id > 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Hypothetical Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Hypothetical Index: ' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - N'' AS index_usage_summary, - N'' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_hypothetical = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; - --Note: disabled NC indexes will have O rows in #IndexSanitySize! - 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 42 AS check_id, - index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Disabled Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Disabled Index:' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - 'DISABLED' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_disabled = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 43: Heaps with forwarded records or deletes', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 43 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heaps with forwarded records or deletes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + ' forwarded fetches, ' - + CAST(h.leaf_delete_count AS NVARCHAR(256)) + ' deletes against heap:' - + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 44: Large Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 44 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Large Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 45 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Medium Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 10000 AND sz.total_rows < 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 46 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Small Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows < 10000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 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 47 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heap with a Nonclustered Primary Key' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' 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 - WHERE i.index_type = 2 AND i.is_primary_key = 1 AND i.secret_columns LIKE '%RID%' - OPTION ( RECOMPILE ); - - END; - ---------------------------------------- - --Indexaphobia - --Missing indexes with value >= 5 million: : Check_id 50-59 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 50: Indexaphobia.', 0,1) WITH NOWAIT; - WITH index_size_cte - AS ( SELECT i.database_id, - i.schema_name, - i.[object_id], - MAX(i.index_sanity_id) AS index_sanity_id, - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days, - ISNULL ( - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) - AS NVARCHAR(30))+ N' NC indexes exist (' + - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 - THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. - - AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' - ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - AS NVARCHAR(30)) + N'MB); ' - END + - CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS MONEY), 1), '.00', '') - END + - + N' Estimated Rows;' - ,N'') AS index_size_summary - FROM #IndexSanity AS i - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id AND i.database_id = sz.database_id - WHERE i.is_hypothetical = 0 - AND i.is_disabled = 0 - GROUP BY i.database_id, i.schema_name, i.[object_id]) - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, - index_usage_summary, index_size_summary, create_tsql, more_info ) - - SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], - index_estimated_impact, t.index_size_summary, create_tsql, more_info - FROM - ( - SELECT ROW_NUMBER() OVER (ORDER BY mi.is_low, magic_benefit_number DESC) AS rownum, - 50 AS check_id, - sz.index_sanity_id, - 10 AS Priority, - N'Indexaphobia' AS findings_group, - N'High value missing index' + CASE mi.is_low - WHEN 0 THEN N' with High Impact' - WHEN 1 THEN N' with Low Impact' - END - AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Indexaphobia' AS URL, - mi.[statement] + - N' Est. benefit per day: ' + - CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number/@DaysUptime) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS details, - missing_index_details AS [definition], - index_estimated_impact, - sz.index_size_summary, - mi.create_tsql, - mi.more_info, - magic_benefit_number, - mi.is_low - FROM #MissingIndexes mi - LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id - AND mi.database_id = sz.database_id - AND mi.schema_name = sz.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) - OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 - ) AS t - WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END - ORDER BY t.is_low, magic_benefit_number DESC - - - END - ---------------------------------------- - --Abnormal Psychology : Check_id 60-79 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 60: XML indexes', 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 60 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'XML Indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - N'' AS index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_XML = 1 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 61: Columnstore indexes', 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 61 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - CASE WHEN i.is_NC_columnstore=1 - THEN N'NC Columnstore Index' - ELSE N'Clustered Columnstore Index' - END AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_NC_columnstore = 1 OR i.is_CX_columnstore=1 - OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 62: Spatial indexes', 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 62 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Spatial indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_spatial = 1 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 63: Compressed indexes', 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 63 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Compressed indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 64: Partitioned', 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 64 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Partitioned indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NOT NULL OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 65: Non-Aligned Partitioned', 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 65 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Non-Aligned index on a partitioned table' AS finding, - i.[database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanity AS iParent ON - i.[object_id]=iParent.[object_id] - AND i.database_id = iParent.database_id - AND i.schema_name = iParent.schema_name - AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ - AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NULL - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently created tables/indexes (1 week)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was created on ' + - CONVERT(NVARCHAR(16),i.create_date,121) + - N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently modified tables/indexes (2 days)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was modified on ' + - CONVERT(NVARCHAR(16),i.modify_date,121) + - N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND /*Exclude recently created tables.*/ - i.create_date < DATEADD(dd,-7,GETDATE()) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; - -- Allowed Ranges: - --int -2,147,483,648 to 2,147,483,647 - --smallint -32,768 to 32,768 - --tinyint 0 to 255 - - 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 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column within ' + - CAST (calc1.percent_remaining AS NVARCHAR(256)) - + N' percent end of range' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - CROSS APPLY ( - SELECT CAST(CASE WHEN ic.increment_value >= 0 - THEN - CASE ic.system_type_name - WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE 999 - END - ELSE --ic.increment_value is negative - CASE ic.system_type_name - WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE -1 - END - END AS NUMERIC(5,1)) AS percent_remaining - ) AS calc1 - WHERE i.index_id IN (1,0) - AND calc1.percent_remaining <= 30 - UNION ALL - SELECT 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column using a negative seed or increment other than 1' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE i.index_id IN (1,0) - AND (ic.seed_value < 0 OR ic.increment_value <> 1) - ORDER BY finding, details DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND collation_name <> @collation - GROUP BY [object_id], - database_id, - schema_name - ) - 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 69 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Column collation does not match database collation' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name - + N' has ' + CAST(column_count AS NVARCHAR(20)) - + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END - + N' with a different collation than the db collation of ' - + @collation AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.schema_name = i.schema_name - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count, - SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY object_id, - database_id, - schema_name - ) - 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 70 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Replicated columns' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name - + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) - + N' out of ' + CAST(column_count AS NVARCHAR(20)) - + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END - + N' in one or more publications.' - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND i.database_id = cc.database_id - AND i.schema_name = cc.schema_name - WHERE i.index_id IN (1,0) - AND replicated_column_count > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 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, more_info ) - SELECT 71 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Cascading Updates or Deletes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - N'Foreign Key ' + foreign_key_name + - N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' - + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' - + N' has settings:' - + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END - + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END - AS details, - [fk].[database_name] - AS index_definition, - N'N/A' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary, - (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) - AS more_info - FROM #ForeignKeys fk - WHERE ([delete_referential_action_desc] <> N'NO_ACTION' - OR [update_referential_action_desc] <> N'NO_ACTION') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; - IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) - AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) - BEGIN - 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 72 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - 'Columnstore Indexes are being used in conjunction with trace flag 834. Visit the link to see why this can be a bad idea' AS finding, - [database_name] AS [Database Name], - N'https://support.microsoft.com/en-us/kb/3210239' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type IN (5,6) - OPTION ( RECOMPILE ) - END - - END - - ---------------------------------------- - --Workaholics: Check_id 80-89 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 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 ) - - --Workaholics according to index_usage_stats - --This isn't perfect: it mentions the number of scans present in a plan - --A "scan" isn't necessarily a full scan, but hey, we gotta do the best with what we've got. - --in the case of things like indexed views, the operator might be in the plan but never executed - SELECT TOP 5 - 80 AS check_id, - i.index_sanity_id AS index_sanity_id, - 200 AS Priority, - N'Workaholics' AS findings_group, - N'Scan-a-lots (index_usage_stats)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, - REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') - + N' scans against ' + i.db_schema_object_indexid - + N'. Latest scan: ' + ISNULL(CAST(i.last_user_scan AS NVARCHAR(128)),'?') + N'. ' - + N'ScanFactor=' + CAST(((i.user_scans * iss.total_reserved_MB)/1000000.) AS NVARCHAR(256)) AS details, - ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, - ISNULL(i.secret_columns,'') AS secret_columns, - i.index_usage_summary AS index_usage_summary, - iss.index_size_summary AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id - WHERE ISNULL(i.user_scans,0) > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.user_scans * iss.total_reserved_MB DESC; - - RAISERROR(N'check_id 81: Top recent accesses (op stats)', 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 ) - --Workaholics according to index_operational_stats - --This isn't perfect either: range_scan_count contains full scans, partial scans, even seeks in nested loop ops - --But this can help bubble up some most-accessed tables - SELECT TOP 5 - 81 AS check_id, - i.index_sanity_id AS index_sanity_id, - 200 AS Priority, - N'Workaholics' AS findings_group, - N'Top recent accesses (index_op_stats)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, - ISNULL(REPLACE( - CONVERT(NVARCHAR(50),CAST((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), - N'.00',N'') - + N' uses of ' + i.db_schema_object_indexid + N'. ' - + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_range_scan_count AS MONEY),1),N'.00',N'') + N' scans or seeks. ' - + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups. ' - + N'OpStatsFactor=' + CAST(((((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB))/1000000.) AS VARCHAR(256)),'') AS details, - ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, - ISNULL(i.secret_columns,'') AS secret_columns, - i.index_usage_summary AS index_usage_summary, - iss.index_size_summary AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id - WHERE (ISNULL(iss.total_range_scan_count,0) > 0 OR ISNULL(iss.total_singleton_lookup_count,0) > 0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC; - - - END - - ---------------------------------------- - --Statistics Info: Check_id 90-99 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 90: Outdated 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 ) - SELECT 90 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Statistic Abandonment Issues', - s.database_name, - '' AS URL, - 'Statistics on this table were last updated ' + - CASE s.last_statistics_update WHEN NULL THEN N' NEVER ' - ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + - ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + - ' modifications in that time, which is ' + - CONVERT(NVARCHAR(100), s.percent_modifications) + - '% of the table.' - END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 7) - AND s.percent_modifications >= 10. - AND s.rows >= 10000 - - RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Antisocial Samples', - s.database_name, - '' AS URL, - 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows_sampled < 1. - AND s.rows >= 10000 - - 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 ) - SELECT 92 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Cyberphobic Samples', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.no_recompute = 1 - - RAISERROR(N'check_id 93: Statistics with filters', 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 93 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Filter Fixation', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is filtered on [' + s.filter_definition + ']. It could be part of a filtered index, or just a filtered statistic. This is purely informational.' 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_filter = 1 - - END - - ---------------------------------------- - --Computed Column Info: Check_id 99-109 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 99: Computed Columns 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 ) - SELECT 99 AS check_id, - 50 AS Priority, - 'Cold Calculators' AS findings_group, - 'Serial Forcer' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition - + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_function = 1 - - RAISERROR(N'check_id 100: Computed Columns that are not Persisted.', 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 100 AS check_id, - 200 AS Priority, - 'Cold Calculators' AS findings_group, - 'Definition Defeatists' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is not persisted, which means it will be calculated when a query runs.' + - 'You can change this with the following command, if the definition is deterministic: ALTER TABLE ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' ALTER COLUMN ' + cc.column_name + - ' ADD PERSISTED' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_persisted = 0 - - ---------------------------------------- - --Temporal Table Info: Check_id 110-119 - ---------------------------------------- - RAISERROR(N'check_id 110: Temporal Tables.', 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 110 AS check_id, - 200 AS Priority, - 'Temporal Tables' AS findings_group, - 'Obsessive Compulsive Tables', - t.database_name, - '' AS URL, - 'The table ' + QUOTENAME(t.schema_name) + '.' + QUOTENAME(t.table_name) + ' is a temporal table, with rows versioned in ' - + QUOTENAME(t.history_schema_name) + '.' + QUOTENAME(t.history_table_name) + ' on History columns ' + QUOTENAME(t.start_column_name) + ' and ' + QUOTENAME(t.end_column_name) + '.' - AS details, - '' AS index_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #TemporalTables AS t - - - - END - - RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; - IF DATEDIFF(MM, @VersionDate, GETDATE()) > 6 - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - 'Outdated sp_BlitzIndex', 'sp_BlitzIndex is Over 6 Months Old', 'http://FirstResponderKit.org/', - 'Fine wine gets better with age, but this ' + @ScriptVersionName + ' is more like bad cheese. Time to get a new one.', - N'',N'',N'' - ); - END - - IF EXISTS(SELECT * FROM #BlitzIndexResults) - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' - ); - END - ELSE IF @Mode = 0 OR (@GetAllDatabases = 1 AND @Mode <> 4) - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'No Major Problems Found', - 'Nice Work!', - 'http://FirstResponderKit.org', 'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', 'The new default Mode 0 only looks for very serious index issues.', '', '' - ); - - END - ELSE - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'No Problems Found', - 'Nice job! Or more likely, you have a nearly empty database.', - 'http://FirstResponderKit.org', 'Time to go read some blog posts.', '', '', '' - ); - - END - - RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; - - /*Return results.*/ - IF (@Mode = 0) - BEGIN - - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - WHERE br.check_id IN (0, 1, 11, 22, 43, 68, 50, 60, 61, 62, 63, 64, 65, 72) - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END - ELSE IF (@Mode = 4) - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END; /* End @Mode=0 or 4 (diagnose)*/ - ELSE IF @Mode=1 /*Summarize*/ - BEGIN - --This mode is to give some overall stats on the database. - RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; - - SELECT DB_NAME(i.database_id) AS [Database Name], - CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], - CAST(CAST(SUM(sz.total_reserved_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], - CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], - CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], - CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], - CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], - SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN - CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) - ELSE 0 END AS [ratio table: NC Indexes], - SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], - CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Heaps GB], - SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], - SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], - CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], - SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], - SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], - MAX(total_rows) AS [Max Row Count], - CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max Table GB], - CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], - MIN(create_date) AS [Oldest Create Date], - MAX(create_date) AS [Most Recent Create Date], - MAX(modify_date) AS [Most Recent Modify Date], - 1 AS [Display Order] - FROM #IndexSanity AS i - --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz - ON i.index_sanity_id=sz.index_sanity_id - GROUP BY DB_NAME(i.database_id) - UNION ALL - SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,0 AS display_order - ORDER BY [Display Order] ASC - OPTION (RECOMPILE); - - END /* End @Mode=1 (summarize)*/ - ELSE IF @Mode=2 /*Index Detail*/ - BEGIN - --This mode just spits out all the detail without filters. - --This supports slicing AND dicing in Excel - RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; - - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT - DECLARE @ValidOutputLocation BIT - DECLARE @LinkedServerDBCheck NVARCHAR(2000) - DECLARE @ValidLinkedServerDB INT - DECLARE @tmpdbchk table (cnt int) - DECLARE @StringToExecute NVARCHAR(MAX); - - IF @OutputServerName IS NOT NULL - BEGIN - IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END - ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) - BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')' - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk) - IF (@ValidLinkedServerDB > 0) - BEGIN - SET @ValidOutputServer = 1 - SET @ValidOutputLocation = 1 - END - ELSE - RAISERROR('The specified database was not found on the output server', 16, 0) - END - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0) - END - END - ELSE - BEGIN - IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - EXEC(@StringToExecute); - - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - SET @OutputDatabaseName = '[tempdb]'; - SET @OutputSchemaName = '[dbo]'; - SET @ValidOutputLocation = 1; - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1 - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0) - END - ELSE - BEGIN - SET @ValidOutputLocation = 0 - END - END - - /* @OutputTableName lets us export the results to a permanent table */ - DECLARE @RunID UNIQUEIDENTIFIER; - SET @RunID = NEWID(); - - IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) - BEGIN - DECLARE @TableExists BIT; - DECLARE @SchemaExists BIT; - SET @StringToExecute = - N'SET @SchemaExists = 0; - SET @TableExists = 0; - IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - SET @SchemaExists = 1 - IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 1'; - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT - - IF @SchemaExists = 1 - BEGIN - IF @TableExists = 0 - BEGIN - SET @StringToExecute = - N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ - ( - [id] INT IDENTITY(1,1) NOT NULL, - [run_id] UNIQUEIDENTIFIER, - [run_datetime] DATETIME, - [server_name] NVARCHAR(128), - [database_name] NVARCHAR(128), - [schema_name] NVARCHAR(128), - [table_name] NVARCHAR(128), - [index_name] NVARCHAR(128), - [index_id] INT, - [db_schema_object_indexid] NVARCHAR(500), - [object_type] NVARCHAR(15), - [index_definition] NVARCHAR(4000), - [key_column_names_with_sort_order] NVARCHAR(MAX), - [count_key_columns] INT, - [include_column_names] NVARCHAR(MAX), - [count_included_columns] INT, - [secret_columns] NVARCHAR(MAX), - [count_secret_columns] INT, - [partition_key_column_name] NVARCHAR(MAX), - [filter_definition] NVARCHAR(MAX), - [is_indexed_view] BIT, - [is_primary_key] BIT, - [is_XML] BIT, - [is_spatial] BIT, - [is_NC_columnstore] BIT, - [is_CX_columnstore] BIT, - [is_disabled] BIT, - [is_hypothetical] BIT, - [is_padded] BIT, - [fill_factor] INT, - [is_referenced_by_foreign_key] BIT, - [last_user_seek] DATETIME, - [last_user_scan] DATETIME, - [last_user_lookup] DATETIME, - [last_user_update] DATETIME, - [total_reads] BIGINT, - [user_updates] BIGINT, - [reads_per_write] MONEY, - [index_usage_summary] NVARCHAR(200), - [partition_count] INT, - [total_rows] BIGINT, - [total_reserved_MB] NUMERIC(29,2), - [total_reserved_LOB_MB] NUMERIC(29,2), - [total_reserved_row_overflow_MB] NUMERIC(29,2), - [index_size_summary] NVARCHAR(300), - [total_row_lock_count] BIGINT, - [total_row_lock_wait_count] BIGINT, - [total_row_lock_wait_in_ms] BIGINT, - [avg_row_lock_wait_in_ms] BIGINT, - [total_page_lock_count] BIGINT, - [total_page_lock_wait_count] BIGINT, - [total_page_lock_wait_in_ms] BIGINT, - [avg_page_lock_wait_in_ms] BIGINT, - [total_index_lock_promotion_attempt_count] BIGINT, - [total_index_lock_promotion_count] BIGINT, - [data_compression_desc] VARCHAR(8000), - [create_date] DATETIME, - [modify_date] DATETIME, - [more_info] NVARCHAR(500), - [display_order] INT, - CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) - );' - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) - - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,'''','''''') - EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END - ELSE - BEGIN - EXEC(@StringToExecute); - END - END /* @TableExists = 0 */ - - SET @StringToExecute = - N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 0 - ELSE - SET @TableExists = 1'; - - SET @TableExists = NULL - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT', @TableExists OUTPUT - - IF @TableExists = 1 - BEGIN - SET @StringToExecute = - N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ - ( - [run_id], - [run_datetime], - [server_name], - [database_name], - [schema_name], - [table_name], - [index_name], - [index_id], - [db_schema_object_indexid], - [object_type], - [index_definition], - [key_column_names_with_sort_order], - [count_key_columns], - [include_column_names], - [count_included_columns], - [secret_columns], - [count_secret_columns], - [partition_key_column_name], - [filter_definition], - [is_indexed_view], - [is_primary_key], - [is_XML], - [is_spatial], - [is_NC_columnstore], - [is_CX_columnstore], - [is_disabled], - [is_hypothetical], - [is_padded], - [fill_factor], - [is_referenced_by_foreign_key], - [last_user_seek], - [last_user_scan], - [last_user_lookup], - [last_user_update], - [total_reads], - [user_updates], - [reads_per_write], - [index_usage_summary], - [partition_count], - [total_rows], - [total_reserved_MB], - [total_reserved_LOB_MB], - [total_reserved_row_overflow_MB], - [index_size_summary], - [total_row_lock_count], - [total_row_lock_wait_count], - [total_row_lock_wait_in_ms], - [avg_row_lock_wait_in_ms], - [total_page_lock_count], - [total_page_lock_wait_count], - [total_page_lock_wait_in_ms], - [avg_page_lock_wait_in_ms], - [total_index_lock_promotion_attempt_count], - [total_index_lock_promotion_count], - [data_compression_desc], - [create_date], - [modify_date], - [more_info], - [display_order] - ) - SELECT ''@@@RunID@@@'', - ''@@@GETDATE@@@'', - ''@@@LocalServerName@@@'', - -- Below should be a copy/paste of the real query - -- Make sure all quotes are escaped - i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '''') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN ''TABLE'' - ELSE ''NonClustered'' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '''') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '''') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'''') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '''') AS [Partition Key Column Name], - ISNULL(filter_definition, '''') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE);'; - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - EXEC(@StringToExecute); - END /* @TableExists = 1 */ - ELSE - RAISERROR('Creation of the output table failed.', 16, 0) - END /* @TableExists = 0 */ - ELSE - RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0) - END /* @ValidOutputLocation = 1 */ - ELSE - - - SELECT i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' - ELSE 'NonClustered' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], - ISNULL(filter_definition, '') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE); - - - - END /* End @Mode=2 (index detail)*/ - ELSE IF @Mode=3 /*Missing index Detail*/ - BEGIN - - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT - mi.database_name AS [Database Name], - mi.[schema_name] AS [Schema], - mi.table_name AS [Table], - CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) - AS [Magic Benefit Number], - mi.missing_index_details AS [Missing Index Details], - mi.avg_total_user_cost AS [Avg Query Cost], - mi.avg_user_impact AS [Est Index Improvement], - mi.user_seeks AS [Seeks], - mi.user_scans AS [Scans], - mi.unique_compiles AS [Compiles], - mi.equality_columns AS [Equality Columns], - mi.inequality_columns AS [Inequality Columns], - mi.included_columns AS [Included Columns], - mi.index_estimated_impact AS [Estimated Impact], - mi.create_tsql AS [Create TSQL], - mi.more_info AS [More Info], - 1 AS [Display Order], - mi.is_low - FROM #MissingIndexes AS mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - UNION ALL - SELECT - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - 100000000000, - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL, 0 AS [Display Order], NULL AS is_low - ORDER BY [Display Order] ASC, is_low, [Magic Benefit Number] DESC - OPTION (RECOMPILE); - - END /* End @Mode=3 (index detail)*/ -END -END TRY - -BEGIN CATCH - RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; - - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; - END CATCH; -GO -IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); -GO - -ALTER PROCEDURE dbo.sp_BlitzLock -( - @Top INT = 2147483647, - @DatabaseName NVARCHAR(256) = NULL, - @StartDate DATETIME = '19000101', - @EndDate DATETIME = '99991231', - @ObjectName NVARCHAR(1000) = NULL, - @StoredProcName NVARCHAR(1000) = NULL, - @AppName NVARCHAR(256) = NULL, - @HostName NVARCHAR(256) = NULL, - @LoginName NVARCHAR(256) = NULL, - @EventSessionPath VARCHAR(256) = 'system_health*.xel', - @Debug BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -) -AS -BEGIN - -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '1.0'; -SET @VersionDate = '20171201'; - - - IF @Help = 1 PRINT ' - /* - sp_BlitzLock from http://FirstResponderKit.org - - This script checks for and analyzes deadlocks from the system health session or a custom extended event path - - Variables 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. - - @ObjectName: If you want to filter to a specific able. - The object name has to be fully qualified ''Database.Schema.Table'' - - @StoredProcName: If you want to search for a single stored proc - The proc name has to be fully qualified ''Database.Schema.Sproc'' - - @AppName: If you want to filter to a specific application - - @HostName: If you want to filter to a specific host - - @LoginName: If you want to filter to a specific login - - @EventSessionPath: If you want to point this at an XE session rather than the system health session. - - - - 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. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - MIT License - - All other copyright for sp_BlitzLock are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - */'; - - - DECLARE @ProductVersion NVARCHAR(128); - DECLARE @ProductVersionMajor FLOAT; - DECLARE @ProductVersionMinor INT; - - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); - - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); - - - IF @ProductVersionMajor < 11.0 - BEGIN - RAISERROR( - 'sp_BlitzLock will throw a bunch of angry errors on versions of SQL Server earlier than 2012.', - 0, - 1) WITH NOWAIT; - RETURN; - END; - - IF @Top IS NULL - SET @Top = 2147483647; - - IF @StartDate IS NULL - SET @StartDate = '19000101'; - - IF @EndDate IS NULL - SET @EndDate = '99991231'; - - - IF OBJECT_ID('tempdb..#deadlock_data') IS NOT NULL - DROP TABLE #deadlock_data; - - IF OBJECT_ID('tempdb..#deadlock_process') IS NOT NULL - DROP TABLE #deadlock_process; - - IF OBJECT_ID('tempdb..#deadlock_stack') IS NOT NULL - DROP TABLE #deadlock_stack; - - IF OBJECT_ID('tempdb..#deadlock_resource') IS NOT NULL - DROP TABLE #deadlock_resource; - - IF OBJECT_ID('tempdb..#deadlock_owner_waiter') IS NOT NULL - DROP TABLE #deadlock_owner_waiter; - - IF OBJECT_ID('tempdb..#deadlock_findings') IS NOT NULL - DROP TABLE #deadlock_findings; - - CREATE TABLE #deadlock_findings - ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - check_id INT NOT NULL, - database_name NVARCHAR(256), - object_name NVARCHAR(1000), - finding_group NVARCHAR(100), - finding NVARCHAR(4000) - ); - - - /*Grab the initial set of XML to parse*/ - WITH xml - AS ( SELECT CONVERT(XML, event_data) AS deadlock_xml - FROM sys.fn_xe_file_target_read_file(@EventSessionPath, NULL, NULL, NULL) ) - SELECT TOP ( @Top ) xml.deadlock_xml - INTO #deadlock_data - FROM xml - WHERE xml.deadlock_xml.value('(/event/@name)[1]', 'VARCHAR(256)') = 'xml_deadlock_report' - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') >= @StartDate - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') < @EndDate - ORDER BY xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') - OPTION ( RECOMPILE ); - - - - /*Parse process and input buffer XML*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.value('@id', 'NVARCHAR(256)') AS id, - ca.dp.value('@currentdb', 'BIGINT') AS database_id, - ca.dp.value('@logused', 'BIGINT') AS log_used, - ca.dp.value('@waitresource', 'NVARCHAR(256)') AS wait_resource, - ca.dp.value('@waittime', 'BIGINT') AS wait_time, - ca.dp.value('@transactionname', 'NVARCHAR(256)') AS transaction_name, - ca.dp.value('@lasttranstarted', 'DATETIME2(7)') AS last_tran_started, - ca.dp.value('@lastbatchstarted', 'DATETIME2(7)') AS last_batch_started, - ca.dp.value('@lastbatchcompleted', 'DATETIME2(7)') AS last_batch_completed, - ca.dp.value('@lockMode', 'NVARCHAR(256)') AS lock_mode, - ca.dp.value('@trancount', 'BIGINT') AS transaction_count, - ca.dp.value('@clientapp', 'NVARCHAR(256)') AS client_app, - ca.dp.value('@hostname', 'NVARCHAR(256)') AS host_name, - ca.dp.value('@loginname', 'NVARCHAR(256)') AS login_name, - ca.dp.value('@isolationlevel', 'NVARCHAR(256)') AS isolation_level, - ca2.ib.query('.') AS input_buffer, - ca.dp.query('.') AS process_xml - INTO #deadlock_process - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process/inputbuf') AS ca2(ib) - WHERE (ca.dp.value('@currentdb', 'BIGINT') = DB_ID(@DatabaseName) OR @DatabaseName IS NULL) - AND (ca.dp.value('@clientapp', 'NVARCHAR(256)') = @AppName OR @AppName IS NULL) - AND (ca.dp.value('@hostname', 'NVARCHAR(256)') = @HostName OR @HostName IS NULL) - AND (ca.dp.value('@loginname', 'NVARCHAR(256)') = @LoginName OR @LoginName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Parse execution stack XML*/ - SELECT dp.id, - dp.event_date, - ca.dp.value('@procname', 'NVARCHAR(1000)') AS proc_name, - ca.dp.value('@sqlhandle', 'NVARCHAR(128)') AS sql_handle - INTO #deadlock_stack - FROM #deadlock_process AS dp - CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) - WHERE (ca.dp.value('@procname', 'NVARCHAR(256)') = @StoredProcName OR @StoredProcName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Grab the full resource list*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.query('.') AS resource_xml - INTO #deadlock_resource - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) - OPTION ( RECOMPILE ); - - - /*This parses object locks*/ - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(1000)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - INTO #deadlock_owner_waiter - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - WHERE (ca.dr.value('@objectname', 'NVARCHAR(1000)') = @ObjectName OR @ObjectName IS NULL) - OPTION ( RECOMPILE ); - - - - /*This parses page locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses key locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses rid locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - /*Get rid of nonsense*/ - DELETE dow - FROM #deadlock_owner_waiter AS dow - WHERE dow.owner_id = dow.waiter_id; - - /*Add some nonsense*/ - ALTER TABLE #deadlock_process - ADD waiter_mode NVARCHAR(256), - owner_mode NVARCHAR(256), - is_victim AS CONVERT(BIT, CASE WHEN id = victim_id THEN 1 ELSE 0 END); - - /*Update some nonsense*/ - UPDATE dp - SET dp.owner_mode = dow.owner_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 0; - - UPDATE dp - SET dp.waiter_mode = dow.waiter_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.victim_id = dow.waiter_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 1; - - - /*Begin checks based on parsed values*/ - - /*Check 1 is deadlocks by database*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 1 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Total database locks' AS finding_group, - 'This database had ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) - + ' deadlocks.' - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - /*Check 2 is deadlocks by object*/ - - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 2 AS check_id, - DB_NAME(dow.database_id) AS database_name, - dow.object_name AS object_name, - 'Total object deadlocks' AS finding_group, - 'This object was involved in ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.object_name)) - + ' deadlock(s).' - FROM #deadlock_owner_waiter AS dow - GROUP BY DB_NAME(dow.database_id), dow.object_name - OPTION ( RECOMPILE ); - - - /*Check 3 looks for Serializable locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 3 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Serializable locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of serializable deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'serializable%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 4 looks for Repeatable Read locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 4 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Repeatable Read locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of repeatable read deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'repeatable read%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 5 breaks down app, host, and login information*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 5 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Login, App, and Host locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) + - ' instances of deadlocks involving the login ' + - ISNULL(dp.login_name, 'UNKNOWN') + - ' from the application ' + - ISNULL(dp.client_app, 'UNKNOWN') + - ' on host ' + - ISNULL(dp.host_name, 'UNKNOWN') - AS finding - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id), dp.login_name, dp.client_app, dp.host_name - OPTION ( RECOMPILE ); - - - /*Check 6 breaks down the types of locks (object, page, key, etc.)*/ - WITH lock_types AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - dow.object_name, - SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) -1) AS lock, - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.id)) AS lock_count - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - GROUP BY DB_NAME(dp.database_id), SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) - 1), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 6 AS check_id, - lt.database_name, - lt.object_name, - 'Types of locks by object' AS finding_group, - 'This object has had ' + - STUFF((SELECT DISTINCT N', ' + lt2.lock_count + ' ' + 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'') - + ' locks' - FROM lock_types AS lt - OPTION ( RECOMPILE ); - - - /*Check 7 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name, - '''' + STUFF((SELECT DISTINCT N',' + ds2.sql_handle - FROM #deadlock_stack AS ds2 - WHERE ds2.id = ds.id - AND ds2.event_date = ds.event_date - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + '''' AS sql_handle_csv - FROM #deadlock_stack AS ds - GROUP BY PARSENAME(ds.proc_name, 3), - PARSENAME(ds.proc_name, 2), - PARSENAME(ds.proc_name, 1), - ds.id, - ds.proc_name, - ds.event_date - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzCache ' + - CASE WHEN ds.proc_name = 'adhoc' - THEN ' @OnlySqlHandles = ' + sql_handle_csv - ELSE '@StoredProcName = ' + - QUOTENAME(ds.proc_only_name, '''') - END + - ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - OPTION ( RECOMPILE ); - - IF @ProductVersionMajor >= 13 - BEGIN - - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.sql_handle, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name - FROM #deadlock_stack AS ds - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzQueryStore ' - + '@DatabaseName = ' - + QUOTENAME(ds.database_name, '''') - + ', ' - + '@StoredProcName = ' - + QUOTENAME(ds.proc_only_name, '''') - + ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - WHERE ds.proc_name <> 'adhoc' - OPTION ( RECOMPILE ); - END; - - - /*Check 8 gives you stored proc deadlock counts*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 8 AS check_id, - DB_NAME(dp.database_id) AS database_name, - ds.proc_name, - 'Stored Procedure Deadlocks', - 'The stored procedure ' - + PARSENAME(ds.proc_name, 2) - + '.' - + PARSENAME(ds.proc_name, 1) - + ' has been involved in ' - + CONVERT(NVARCHAR(10), COUNT_BIG(DISTINCT ds.id)) - + ' deadlocks.' - FROM #deadlock_stack AS ds - JOIN #deadlock_process AS dp - ON dp.id = ds.id - AND ds.event_date = dp.event_date - WHERE ds.proc_name <> 'adhoc' - GROUP BY DB_NAME(dp.database_id), ds.proc_name - OPTION(RECOMPILE); - - - /*Check 9 gives you more info queries for sp_BlitzIndex */ - WITH bi AS ( - SELECT DISTINCT - dow.object_name, - PARSENAME(dow.object_name, 3) AS database_name, - PARSENAME(dow.object_name, 2) AS schema_name, - PARSENAME(dow.object_name, 1) AS table_name - FROM #deadlock_owner_waiter AS dow - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 9 AS check_id, - bi.database_name, - bi.schema_name + '.' + bi.table_name, - 'More Info - Table' AS finding_group, - 'EXEC sp_BlitzIndex ' + - '@DatabaseName = ' + QUOTENAME(bi.database_name, '''') + - ', @SchemaName = ' + QUOTENAME(bi.schema_name, '''') + - ', @TableName = ' + QUOTENAME(bi.table_name, '''') + - ';' AS finding - FROM bi - OPTION ( RECOMPILE ); - - /*Check 10 gets total deadlock wait time per object*/ - WITH chopsuey AS ( - SELECT DISTINCT - PARSENAME(dow.object_name, 3) AS database_name, - dow.object_name, - CONVERT(VARCHAR(10), (SUM(DISTINCT dp.wait_time) / 1000) / 86400) AS wait_days, - CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT dp.wait_time) / 1000), 0), 108) AS wait_time_hms - FROM #deadlock_owner_waiter AS dow - JOIN #deadlock_process AS dp - ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - GROUP BY PARSENAME(dow.object_name, 3), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 10 AS check_id, - cs.database_name, - cs.object_name, - 'Total object deadlock wait time' AS finding_group, - 'This object has had ' - + CONVERT(VARCHAR(10), cs.wait_days) - + ':' + CONVERT(VARCHAR(20), cs.wait_time_hms, 108) - + ' [d/h/m/s] of deadlock wait time.' AS finding - FROM chopsuey AS cs - WHERE cs.object_name IS NOT NULL - OPTION ( RECOMPILE ); - - /*Check 11 gets total deadlock wait time per database*/ - WITH wait_time AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - SUM(CONVERT(BIGINT, dp.wait_time)) AS total_wait_time_ms - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 11 AS check_id, - wt.database_name, - '-' AS object_name, - 'Total database deadlock wait time' AS finding_group, - 'This database has had ' - + CONVERT(VARCHAR(10), (SUM(DISTINCT wt.total_wait_time_ms) / 1000) / 86400) - + ':' + CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT wt.total_wait_time_ms) / 1000), 0), 108) - + ' [d/h/m/s] of deadlock wait time.' - FROM wait_time AS wt - GROUP BY wt.database_name - OPTION ( RECOMPILE ); - - - /*Thank you goodnight*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - VALUES ( -1, - N'sp_BlitzLock ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - N'SQL Server First Responder Kit', - N'http://FirstResponderKit.org/', - N'To get help or add your own contributions, join us at http://FirstResponderKit.org.'); - - - - - /*Results*/ - WITH deadlocks - AS ( SELECT dp.event_date, - dp.id, - dp.victim_id, - dp.database_id, - dp.log_used, - dp.wait_resource, - CONVERT( - XML, - STUFF(( SELECT DISTINCT NCHAR(10) - + N' ' - + ISNULL(c.object_name, N'') - + N' ' AS object_name - FROM #deadlock_owner_waiter AS c - WHERE (dp.id = c.owner_id - OR dp.victim_id = c.waiter_id) - AND dp.event_date = c.event_date - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), - 1, 1, N'')) AS object_names, - dp.wait_time, - dp.transaction_name, - dp.last_tran_started, - dp.last_batch_started, - dp.last_batch_completed, - dp.lock_mode, - dp.transaction_count, - dp.client_app, - dp.host_name, - dp.login_name, - dp.isolation_level, - dp.process_xml.value('(//process/inputbuf/text())[1]', 'NVARCHAR(MAX)') AS inputbuf, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date ) AS dn, - DENSE_RANK() OVER ( ORDER BY dp.event_date ) AS en, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date ORDER BY dp.event_date ) -1 AS qn, - dp.is_victim, - ISNULL(dp.owner_mode, '-') AS owner_mode, - ISNULL(dp.waiter_mode, '-') AS waiter_mode - FROM #deadlock_process AS dp ) - SELECT d.event_date, - DB_NAME(d.database_id) AS database_name, - 'Deadlock #' - + CONVERT(NVARCHAR(10), d.en) - + ', Query #' - + CASE WHEN d.qn = 0 THEN N'1' ELSE CONVERT(NVARCHAR(10), d.qn) END - + CASE WHEN d.is_victim = 1 THEN ' - VICTIM' ELSE '' END - AS deadlock_group, - CONVERT(XML, N'' + d.inputbuf + N'') AS query, - d.object_names, - d.isolation_level, - d.owner_mode, - d.waiter_mode, - d.transaction_count, - d.login_name, - d.host_name, - d.client_app, - d.wait_time, - d.log_used, - d.last_tran_started, - d.last_batch_started, - d.last_batch_completed, - d.transaction_name - FROM deadlocks AS d - WHERE d.dn = 1 - ORDER BY d.event_date, is_victim DESC; - - - - SELECT df.check_id, df.database_name, df.object_name, df.finding_group, df.finding - FROM #deadlock_findings AS df - ORDER BY df.check_id - OPTION ( RECOMPILE ); - - - IF @Debug = 1 - BEGIN - - SELECT '#deadlock_data' AS table_name, * - FROM #deadlock_data AS dd - OPTION ( RECOMPILE ); - - SELECT '#deadlock_resource' AS table_name, * - FROM #deadlock_resource AS dr - OPTION ( RECOMPILE ); - - SELECT '#deadlock_owner_waiter' AS table_name, * - FROM #deadlock_owner_waiter AS dow - OPTION ( RECOMPILE ); - - SELECT '#deadlock_process' AS table_name, * - FROM #deadlock_process AS dp - OPTION ( RECOMPILE ); - - SELECT '#deadlock_stack' AS table_name, * - FROM #deadlock_stack AS ds - OPTION ( RECOMPILE ); - - END; -- End debug - - END; --Final End - -GO - -IF OBJECT_ID('dbo.sp_BlitzWho') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzWho AS RETURN 0;') -GO - -ALTER PROCEDURE dbo.sp_BlitzWho - @Help TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0, - @ExpertMode BIT = 0, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -AS -BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; - - - IF @Help = 1 - PRINT ' -sp_BlitzWho from http://FirstResponderKit.org - -This script gives you a snapshot of everything currently executing on your SQL Server. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - -MIT License - -Copyright (c) 2017 Brent Ozar Unlimited - -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. -'; - -/* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@EnhanceFlag BIT = 0 - ,@StringToExecute NVARCHAR(MAX) - ,@EnhanceSQL NVARCHAR(MAX) = - N'query_stats.last_dop, - query_stats.min_dop, - query_stats.max_dop, - query_stats.last_grant_kb, - query_stats.min_grant_kb, - query_stats.max_grant_kb, - query_stats.last_used_grant_kb, - query_stats.min_used_grant_kb, - query_stats.max_used_grant_kb, - query_stats.last_ideal_grant_kb, - query_stats.min_ideal_grant_kb, - query_stats.max_ideal_grant_kb, - query_stats.last_reserved_threads, - query_stats.min_reserved_threads, - query_stats.max_reserved_threads, - query_stats.last_used_threads, - query_stats.min_used_threads, - query_stats.max_used_threads,' - ,@SessionWaits BIT = 0 - ,@SessionWaitsSQL NVARCHAR(MAX) = - N'LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT TOP 5 waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_time_ms) AS NVARCHAR(128)) - + N'' ms), '' - FROM sys.dm_exec_session_wait_stats AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - HAVING SUM(waitwait.wait_time_ms) > 5 - ORDER BY SUM(waitwait.wait_time_ms) DESC - FOR - XML PATH('''') ) AS session_wait_info - FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 - ON s.session_id = wt2.session_id - LEFT JOIN sys.dm_exec_query_stats AS session_stats - ON r.sql_handle = session_stats.sql_handle - AND r.plan_handle = session_stats.plan_handle - AND r.statement_start_offset = session_stats.statement_start_offset - AND r.statement_end_offset = session_stats.statement_end_offset' - ,@QueryStatsXML BIT = 0 - ,@QueryStatsXMLselect NVARCHAR(MAX) = N' CAST(COALESCE(qs_live.query_plan, '''') AS XML) AS live_query_plan , ' - ,@QueryStatsXMLSQL NVARCHAR(MAX) = N'OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live' - - -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) -IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan') - SET @QueryStatsXML = 1; - - -IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 -BEGIN -SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan , - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), - blocked.waittime) + '')'' ) AS wait_info , - CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name , - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' -END -IF @ProductVersionMajor >= 11 -BEGIN -SELECT @EnhanceFlag = - CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 - WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 - WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 - ELSE 0 - END - - -IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL -BEGIN - SET @SessionWaits = 1 -END - - - -SELECT @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan ,' - + - CASE @QueryStatsXML - WHEN 1 THEN + @QueryStatsXMLselect - ELSE N'' - END - +' - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) AS wait_info ,' - + - CASE @SessionWaits - WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' - ELSE N'' - END - + - N'CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , ' - + - CASE @EnhanceFlag - WHEN 1 THEN @EnhanceSQL - ELSE N'' - END - + - N' - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name, - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - ' - + - CASE @SessionWaits - WHEN 1 THEN @SessionWaitsSQL - ELSE N'' - END - + - ' - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - ' - + - CASE @QueryStatsXML - WHEN 1 THEN @QueryStatsXMLSQL - ELSE N'' - END - + - ' - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' - -END - -IF @Debug = 1 - BEGIN - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 160000)) - END - -EXEC(@StringToExecute); - -END -GO diff --git a/Install-Core-Blitz-With-Query-Store.sql b/Install-Core-Blitz-With-Query-Store.sql deleted file mode 100644 index 2f813a63a..000000000 --- a/Install-Core-Blitz-With-Query-Store.sql +++ /dev/null @@ -1,28730 +0,0 @@ -IF OBJECT_ID('dbo.sp_Blitz') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_Blitz AS RETURN 0;'); -GO - -ALTER PROCEDURE [dbo].[sp_Blitz] - @Help TINYINT = 0 , - @CheckUserDatabaseObjects TINYINT = 1 , - @CheckProcedureCache TINYINT = 0 , - @OutputType VARCHAR(20) = 'TABLE' , - @OutputProcedureCache TINYINT = 0 , - @CheckProcedureCacheFilter VARCHAR(10) = NULL , - @CheckServerInfo TINYINT = 0 , - @SkipChecksServer NVARCHAR(256) = NULL , - @SkipChecksDatabase NVARCHAR(256) = NULL , - @SkipChecksSchema NVARCHAR(256) = NULL , - @SkipChecksTable NVARCHAR(256) = NULL , - @IgnorePrioritiesBelow INT = NULL , - @IgnorePrioritiesAbove INT = NULL , - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @OutputXMLasNVARCHAR TINYINT = 0 , - @EmailRecipients VARCHAR(MAX) = NULL , - @EmailProfile sysname = NULL , - @SummaryMode TINYINT = 0 , - @BringThePain TINYINT = 0 , - @Debug TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; - SET @OutputType = UPPER(@OutputType); - - IF @Help = 1 PRINT ' - /* - sp_Blitz from http://FirstResponderKit.org - - This script checks the health of your SQL Server and gives you a prioritized - to-do list of the most urgent things you should consider fixing. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - If a database name has a question mark in it, some tests will fail. Gotta - love that unsupported sp_MSforeachdb. - - If you have offline databases, sp_Blitz fails the first time you run it, - but does work the second time. (Hoo, boy, this will be fun to debug.) - - @OutputServerName will output QueryPlans as NVARCHAR(MAX) since Microsoft - has refused to support XML columns in Linked Server queries. The bug is now - 16 years old! *~ \o/ ~* - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. - @CheckServerInfo 1=show server info like CPUs, memory, virtualization - @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. - @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm - @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' - @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list | ''SCHEMA''=version and field list | ''NONE'' = none - @IgnorePrioritiesBelow 50=ignore priorities below 50 - @IgnorePrioritiesAbove 50=ignore priorities above 50 - For the rest of the parameters, see https://www.BrentOzar.com/blitz/documentation for details. - - - - MIT License - - Copyright for portions of sp_Blitz are held by Microsoft as part of project - tigertoolbox and are provided under the MIT license: - https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - - - - */'; - ELSE IF @OutputType = 'SCHEMA' - BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT'; - - END; - ELSE /* IF @OutputType = 'SCHEMA' */ - BEGIN - - DECLARE @StringToExecute NVARCHAR(4000) - ,@curr_tracefilename NVARCHAR(500) - ,@base_tracefilename NVARCHAR(500) - ,@indx int - ,@query_result_separator CHAR(1) - ,@EmailSubject NVARCHAR(255) - ,@EmailBody NVARCHAR(MAX) - ,@EmailAttachmentFilename NVARCHAR(255) - ,@ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@CurrentName NVARCHAR(128) - ,@CurrentDefaultValue NVARCHAR(200) - ,@CurrentCheckID INT - ,@CurrentPriority INT - ,@CurrentFinding VARCHAR(200) - ,@CurrentURL VARCHAR(200) - ,@CurrentDetails NVARCHAR(4000) - ,@MsSinceWaitsCleared DECIMAL(38,0) - ,@CpuMsSinceWaitsCleared DECIMAL(38,0) - ,@ResultText NVARCHAR(MAX) - ,@crlf NVARCHAR(2) - ,@Processors int - ,@NUMANodes int - ,@MinServerMemory bigint - ,@MaxServerMemory bigint - ,@ColumnStoreIndexesInUse bit - ,@TraceFileIssue bit - -- Flag for Windows OS to help with Linux support - ,@IsWindowsOperatingSystem BIT; - - - SET @crlf = NCHAR(13) + NCHAR(10); - SET @ResultText = 'sp_Blitz Results: ' + @crlf; - - /* - --TOURSTOP01-- - See https://www.BrentOzar.com/go/blitztour for a guided tour. - - We start by creating #BlitzResults. It's a temp table that will store all of - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. - - #BlitzResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - For a list of checks, visit http://FirstResponderKit.org. - */ - IF OBJECT_ID('tempdb..#BlitzResults') IS NOT NULL - DROP TABLE #BlitzResults; - CREATE TABLE #BlitzResults - ( - ID INT IDENTITY(1, 1) , - CheckID INT , - DatabaseName NVARCHAR(128) , - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL - ); - - IF OBJECT_ID('tempdb..#TemporaryDatabaseResults') IS NOT NULL - DROP TABLE #TemporaryDatabaseResults; - CREATE TABLE #TemporaryDatabaseResults - ( - DatabaseName NVARCHAR(128) , - Finding NVARCHAR(128) - ); - - /* - You can build your own table with a list of checks to skip. For example, you - might have some databases that you don't care about, or some checks you don't - want to run. Then, when you run sp_Blitz, you can specify these parameters: - @SkipChecksDatabase = 'DBAtools', - @SkipChecksSchema = 'dbo', - @SkipChecksTable = 'BlitzChecksToSkip' - Pass in the database, schema, and table that contains the list of checks you - want to skip. This part of the code checks those parameters, gets the list, - and then saves those in a temp table. As we run each check, we'll see if we - need to skip it. - - Really anal-retentive users will note that the @SkipChecksServer parameter is - not used. YET. We added that parameter in so that we could avoid changing the - stored proc's surface area (interface) later. - */ - /* --TOURSTOP07-- */ - IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL - DROP TABLE #SkipChecks; - CREATE TABLE #SkipChecks - ( - DatabaseName NVARCHAR(128) , - CheckID INT , - ServerName NVARCHAR(128) - ); - CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); - - IF @SkipChecksTable IS NOT NULL - AND @SkipChecksSchema IS NOT NULL - AND @SkipChecksDatabase IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Inserting SkipChecks', 0, 1) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) - SELECT DISTINCT DatabaseName, CheckID, ServerName - FROM ' + QUOTENAME(@SkipChecksDatabase) + '.' + QUOTENAME(@SkipChecksSchema) + '.' + QUOTENAME(@SkipChecksTable) - + ' WHERE ServerName IS NULL OR ServerName = SERVERPROPERTY(''ServerName'') OPTION (RECOMPILE);'; - EXEC(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - BEGIN - -- Flag for Windows OS to help with Linux support - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN - SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; - END; - ELSE - BEGIN - SELECT @IsWindowsOperatingSystem = 1 ; - END; - - select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; - set @curr_tracefilename = reverse(@curr_tracefilename); - - -- Set the trace file path separator based on underlying OS - IF (@IsWindowsOperatingSystem = 1) - BEGIN - select @indx = patindex('%\%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '\log.trc' ; - END; - ELSE - BEGIN - select @indx = patindex('%/%', @curr_tracefilename) ; - set @curr_tracefilename = reverse(@curr_tracefilename) ; - set @base_tracefilename = left( @curr_tracefilename,len(@curr_tracefilename) - @indx) + '/log.trc' ; - END; - - END; - - /* If the server has any databases on Antiques Roadshow, skip the checks that would break due to CTEs. */ - IF @CheckUserDatabaseObjects = 1 AND EXISTS(SELECT * FROM sys.databases WHERE compatibility_level < 90) - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Databases with compatibility level < 90 found, so setting @CheckUserDatabaseObjects = 0.'; - PRINT 'The database-level checks rely on CTEs, which are not supported in SQL 2000 compat level databases.'; - PRINT 'Get with the cool kids and switch to a current compatibility level, Grandpa. To find the problems, run:'; - PRINT 'SELECT * FROM sys.databases WHERE compatibility_level < 90;'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 204 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'Since you have databases with compatibility_level < 90, we can''t run @CheckUserDatabaseObjects = 1. To find them: SELECT * FROM sys.databases WHERE compatibility_level < 90' AS Details; - END; - - - /* --TOURSTOP08-- */ - /* If the server is Amazon RDS, skip checks that it doesn't allow */ - IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('ServerName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (6); - INSERT INTO #SkipChecks (CheckID) VALUES (29); - INSERT INTO #SkipChecks (CheckID) VALUES (30); - INSERT INTO #SkipChecks (CheckID) VALUES (31); - INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file */ - INSERT INTO #SkipChecks (CheckID) VALUES (57); - INSERT INTO #SkipChecks (CheckID) VALUES (59); - INSERT INTO #SkipChecks (CheckID) VALUES (61); - INSERT INTO #SkipChecks (CheckID) VALUES (62); - INSERT INTO #SkipChecks (CheckID) VALUES (68); - INSERT INTO #SkipChecks (CheckID) VALUES (69); - INSERT INTO #SkipChecks (CheckID) VALUES (73); - INSERT INTO #SkipChecks (CheckID) VALUES (79); - INSERT INTO #SkipChecks (CheckID) VALUES (92); - INSERT INTO #SkipChecks (CheckID) VALUES (94); - INSERT INTO #SkipChecks (CheckID) VALUES (96); - INSERT INTO #SkipChecks (CheckID) VALUES (98); - INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled */ - INSERT INTO #SkipChecks (CheckID) VALUES (123); - INSERT INTO #SkipChecks (CheckID) VALUES (177); - INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans */ - INSERT INTO #SkipChecks (CheckID) VALUES (181); - END; /* Amazon RDS skipped checks */ - - /* If the server is ExpressEdition, skip checks that it doesn't allow */ - IF CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%' - BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (30); /* Alerts not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (31); /* Operators not configured */ - INSERT INTO #SkipChecks (CheckID) VALUES (61); /* Agent alerts 19-25 */ - INSERT INTO #SkipChecks (CheckID) VALUES (73); /* Failsafe operator */ - INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ - END; /* Express Edition skipped checks */ - - - /* - That's the end of the SkipChecks stuff. - The next several tables are used by various checks later. - */ - IF OBJECT_ID('tempdb..#ConfigurationDefaults') IS NOT NULL - DROP TABLE #ConfigurationDefaults; - CREATE TABLE #ConfigurationDefaults - ( - name NVARCHAR(128) , - DefaultValue BIGINT, - CheckID INT - ); - - IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL - DROP TABLE #Recompile; - CREATE TABLE #Recompile( - DBName varchar(200), - ProcName varchar(300), - RecompileFlag varchar(1), - SPSchema varchar(50) - ); - - IF OBJECT_ID('tempdb..#DatabaseDefaults') IS NOT NULL - DROP TABLE #DatabaseDefaults; - CREATE TABLE #DatabaseDefaults - ( - name NVARCHAR(128) , - DefaultValue NVARCHAR(200), - CheckID INT, - Priority INT, - Finding VARCHAR(200), - URL VARCHAR(200), - Details NVARCHAR(4000) - ); - - IF OBJECT_ID('tempdb..#DatabaseScopedConfigurationDefaults') IS NOT NULL - DROP TABLE #DatabaseScopedConfigurationDefaults; - CREATE TABLE #DatabaseScopedConfigurationDefaults - (ID INT IDENTITY(1,1), configuration_id INT, [name] NVARCHAR(60), default_value sql_variant, default_value_for_secondary sql_variant, CheckID INT, ); - - - - IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL - DROP TABLE #DBCCs; - CREATE TABLE #DBCCs - ( - ID INT IDENTITY(1, 1) - PRIMARY KEY , - ParentObject VARCHAR(255) , - Object VARCHAR(255) , - Field VARCHAR(255) , - Value VARCHAR(255) , - DbName NVARCHAR(128) NULL - ); - - - IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL - DROP TABLE #LogInfo2012; - CREATE TABLE #LogInfo2012 - ( - recoveryunitid INT , - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); - - IF OBJECT_ID('tempdb..#LogInfo') IS NOT NULL - DROP TABLE #LogInfo; - CREATE TABLE #LogInfo - ( - FileID SMALLINT , - FileSize BIGINT , - StartOffset BIGINT , - FSeqNo BIGINT , - [Status] TINYINT , - Parity TINYINT , - CreateLSN NUMERIC(38) - ); - - IF OBJECT_ID('tempdb..#partdb') IS NOT NULL - DROP TABLE #partdb; - CREATE TABLE #partdb - ( - dbname NVARCHAR(128) , - objectname NVARCHAR(200) , - type_desc NVARCHAR(128) - ); - - IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); - - IF OBJECT_ID('tempdb..#driveInfo') IS NOT NULL - DROP TABLE #driveInfo; - CREATE TABLE #driveInfo - ( - drive NVARCHAR , - SIZE DECIMAL(18, 2) - ); - - - IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL - DROP TABLE #dm_exec_query_stats; - CREATE TABLE #dm_exec_query_stats - ( - [id] [int] NOT NULL - IDENTITY(1, 1) , - [sql_handle] [varbinary](64) NOT NULL , - [statement_start_offset] [int] NOT NULL , - [statement_end_offset] [int] NOT NULL , - [plan_generation_num] [bigint] NOT NULL , - [plan_handle] [varbinary](64) NOT NULL , - [creation_time] [datetime] NOT NULL , - [last_execution_time] [datetime] NOT NULL , - [execution_count] [bigint] NOT NULL , - [total_worker_time] [bigint] NOT NULL , - [last_worker_time] [bigint] NOT NULL , - [min_worker_time] [bigint] NOT NULL , - [max_worker_time] [bigint] NOT NULL , - [total_physical_reads] [bigint] NOT NULL , - [last_physical_reads] [bigint] NOT NULL , - [min_physical_reads] [bigint] NOT NULL , - [max_physical_reads] [bigint] NOT NULL , - [total_logical_writes] [bigint] NOT NULL , - [last_logical_writes] [bigint] NOT NULL , - [min_logical_writes] [bigint] NOT NULL , - [max_logical_writes] [bigint] NOT NULL , - [total_logical_reads] [bigint] NOT NULL , - [last_logical_reads] [bigint] NOT NULL , - [min_logical_reads] [bigint] NOT NULL , - [max_logical_reads] [bigint] NOT NULL , - [total_clr_time] [bigint] NOT NULL , - [last_clr_time] [bigint] NOT NULL , - [min_clr_time] [bigint] NOT NULL , - [max_clr_time] [bigint] NOT NULL , - [total_elapsed_time] [bigint] NOT NULL , - [last_elapsed_time] [bigint] NOT NULL , - [min_elapsed_time] [bigint] NOT NULL , - [max_elapsed_time] [bigint] NOT NULL , - [query_hash] [binary](8) NULL , - [query_plan_hash] [binary](8) NULL , - [query_plan] [xml] NULL , - [query_plan_filtered] [nvarchar](MAX) NULL , - [text] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL , - [text_filtered] [nvarchar](MAX) COLLATE SQL_Latin1_General_CP1_CI_AS - NULL - ); - - IF OBJECT_ID('tempdb..#ErrorLog') IS NOT NULL - DROP TABLE #ErrorLog; - CREATE TABLE #ErrorLog - ( - LogDate DATETIME , - ProcessInfo NVARCHAR(20) , - [Text] NVARCHAR(1000) - ); - - IF OBJECT_ID('tempdb..#fnTraceGettable') IS NOT NULL - DROP TABLE #fnTraceGettable; - CREATE TABLE #fnTraceGettable - ( - TextData NVARCHAR(4000) , - DatabaseName NVARCHAR(256) , - EventClass INT , - Severity INT , - StartTime DATETIME , - EndTime DATETIME , - Duration BIGINT , - NTUserName NVARCHAR(256) , - NTDomainName NVARCHAR(256) , - HostName NVARCHAR(256) , - ApplicationName NVARCHAR(256) , - LoginName NVARCHAR(256) , - DBUserName NVARCHAR(256) - ); - - IF OBJECT_ID('tempdb..#IgnorableWaits') IS NOT NULL - DROP TABLE #IgnorableWaits; - CREATE TABLE #IgnorableWaits (wait_type NVARCHAR(60)); - INSERT INTO #IgnorableWaits VALUES ('BROKER_EVENTHANDLER'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_RECEIVE_WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TASK_STOP'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TO_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('BROKER_TRANSMITTER'); - INSERT INTO #IgnorableWaits VALUES ('CHECKPOINT_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('CLR_AUTO_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_MANUAL_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('CLR_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_EVENT'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_DBM_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_EVENTS_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRROR_WORKER_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('DBMIRRORING_CMD'); - INSERT INTO #IgnorableWaits VALUES ('DIRTY_PAGE_POLL'); - INSERT INTO #IgnorableWaits VALUES ('DISPATCHER_QUEUE_SEMAPHORE'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('FT_IFTSHC_MUTEX'); - INSERT INTO #IgnorableWaits VALUES ('HADR_CLUSAPI_CALL'); - INSERT INTO #IgnorableWaits VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION'); - INSERT INTO #IgnorableWaits VALUES ('HADR_LOGCAPTURE_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('HADR_NOTIFICATION_DEQUEUE'); - INSERT INTO #IgnorableWaits VALUES ('HADR_TIMER_TASK'); - INSERT INTO #IgnorableWaits VALUES ('HADR_WORK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('LAZYWRITER_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('LOGMGR_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('ONDEMAND_TASK_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM'); - INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS'); - INSERT INTO #IgnorableWaits VALUES ('QDS_ASYNC_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('QDS_SHUTDOWN_QUEUE'); - INSERT INTO #IgnorableWaits VALUES ('REDO_THREAD_PENDING_WORK'); - INSERT INTO #IgnorableWaits VALUES ('REQUEST_FOR_DEADLOCK_SEARCH'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_SYSTEMTASK'); - INSERT INTO #IgnorableWaits VALUES ('SLEEP_TASK'); - INSERT INTO #IgnorableWaits VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_BUFFER_FLUSH'); - INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'); - INSERT INTO #IgnorableWaits VALUES ('UCS_SESSION_REGISTRATION'); - INSERT INTO #IgnorableWaits VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'); - INSERT INTO #IgnorableWaits VALUES ('WAITFOR'); - INSERT INTO #IgnorableWaits VALUES ('XE_DISPATCHER_WAIT'); - INSERT INTO #IgnorableWaits VALUES ('XE_LIVE_TARGET_TVF'); - INSERT INTO #IgnorableWaits VALUES ('XE_TIMER_EVENT'); - - IF @Debug IN (1, 2) RAISERROR('Setting @MsSinceWaitsCleared', 0, 1) WITH NOWAIT; - - SELECT @MsSinceWaitsCleared = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) * 60000.0 - FROM sys.databases - WHERE name = 'tempdb'; - - /* Have they cleared wait stats? Using a 10% fudge factor */ - IF @MsSinceWaitsCleared * .9 > (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 185) WITH NOWAIT; - - SET @MsSinceWaitsCleared = (SELECT MAX(wait_time_ms) FROM sys.dm_os_wait_stats WHERE wait_type IN ('SP_SERVER_DIAGNOSTICS_SLEEP', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP', 'REQUEST_FOR_DEADLOCK_SEARCH', 'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'LAZYWRITER_SLEEP', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP', 'DIRTY_PAGE_POLL', 'LOGMGR_QUEUE')); - IF @MsSinceWaitsCleared = 0 SET @MsSinceWaitsCleared = 1; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES( 185, - 240, - 'Wait Stats', - 'Wait Stats Have Been Cleared', - 'https://BrentOzar.com/go/waits', - 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + CONVERT(NVARCHAR(100), DATEADD(ms, (-1 * @MsSinceWaitsCleared), GETDATE()), 120)); - END; - - /* @CpuMsSinceWaitsCleared is used for waits stats calculations */ - - IF @Debug IN (1, 2) RAISERROR('Setting @CpuMsSinceWaitsCleared', 0, 1) WITH NOWAIT; - - SELECT @CpuMsSinceWaitsCleared = @MsSinceWaitsCleared * scheduler_count - FROM sys.dm_os_sys_info; - - - /* If we're outputting CSV or Markdown, don't bother checking the plan cache because we cannot export plans. */ - IF @OutputType = 'CSV' OR @OutputType = 'MARKDOWN' - SET @CheckProcedureCache = 0; - - /* If we're posting a question on Stack, include background info on the server */ - IF @OutputType = 'MARKDOWN' - SET @CheckServerInfo = 1; - - - /* Only run CheckUserDatabaseObjects if there are less than 50 databases. */ - IF @BringThePain = 0 AND 50 <= (SELECT COUNT(*) FROM sys.databases) AND @CheckUserDatabaseObjects = 1 - BEGIN - SET @CheckUserDatabaseObjects = 0; - PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary insanity for the server and/or user.'; - PRINT 'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.'; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 201 AS CheckID , - 0 AS Priority , - 'Informational' AS FindingsGroup , - '@CheckUserDatabaseObjects Disabled' AS Finding , - 'https://www.BrentOzar.com/blitz/' AS URL , - 'If you want to check 50+ databases, you have to also use @BringThePain = 1.' AS Details; - END; - - /* Sanitize our inputs */ - SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); - - /* Get the major and minor build numbers */ - - IF @Debug IN (1, 2) RAISERROR('Getting version information.', 0, 1) WITH NOWAIT; - - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(varchar(32), @ProductVersion), 2); - - /* - Whew! we're finally done with the setup, and we can start doing checks. - First, let's make sure we're actually supposed to do checks on this server. - The user could have passed in a SkipChecks table that specified to skip ALL - checks on this server, so let's check for that: - */ - IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID IS NULL ) ) - OR ( @SkipChecksTable IS NULL ) - ) - BEGIN - - /* - Our very first check! We'll put more comments in this one just to - explain exactly how it works. First, we check to see if we're - supposed to skip CheckID 1 (that's the check we're working on.) - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 1 ) - BEGIN - - /* - Below, we check master.sys.databases looking for databases - that haven't had a backup in the last week. If we find any, - we insert them into #BlitzResults, the temp table that - tracks our server's problems. Note that if the check does - NOT find any problems, we don't save that. We're only - saving the problems, not the successful checks. - */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 1) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 1 AS CheckID , - d.[name] AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backups Not Performed Recently' AS Finding , - 'https://BrentOzar.com/go/nobak' AS URL , - 'Last backed up: ' - + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details - FROM master.sys.databases d - LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'D' - AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ - WHERE d.database_id <> 2 /* Bonus points if you know what that means */ - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 1) - /* - The above NOT IN filters out the databases we're not supposed to check. - */ - GROUP BY d.name - HAVING MAX(b.backup_finish_date) <= DATEADD(dd, - -7, GETDATE()) - OR MAX(b.backup_finish_date) IS NULL; - /* - And there you have it. The rest of this stored procedure works the same - way: it asks: - - Should I skip this check? - - If not, do I find problems? - - Insert the results into #BlitzResults - */ - - END; - - /* - And that's the end of CheckID #1. - - CheckID #2 is a little simpler because it only involves one query, and it's - more typical for queries that people contribute. But keep reading, because - the next check gets more complex again. - */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 2 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 2 AS CheckID , - d.name AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Full Recovery Model w/o Log Backups' AS Finding , - 'https://BrentOzar.com/go/biglogs' AS URL , - ( 'The ' + CAST(CAST((SELECT ((SUM([mf].[size]) * 8.) / 1024.) FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = d.[database_id] AND [mf].[type_desc] = 'LOG') AS DECIMAL(18,2)) AS VARCHAR(30)) + 'MB log file has not been backed up in the last week.' ) AS Details - FROM master.sys.databases d - WHERE d.recovery_model IN ( 1, 2 ) - AND d.database_id NOT IN ( 2, 3 ) - AND d.source_database_id IS NULL - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 2) - AND NOT EXISTS ( SELECT * - FROM msdb.dbo.backupset b - WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'L' - AND b.backup_finish_date >= DATEADD(dd, - -7, GETDATE()) ); - END; - - - /* - Next up, we've got CheckID 8. (These don't have to go in order.) This one - won't work on SQL Server 2005 because it relies on a new DMV that didn't - exist prior to SQL Server 2008. This means we have to check the SQL Server - version first, then build a dynamic string with the query we want to run: - */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 8 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 8) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, Priority, - FindingsGroup, - Finding, URL, - Details) - SELECT 8 AS CheckID, - 230 AS Priority, - ''Security'' AS FindingsGroup, - ''Server Audits Running'' AS Finding, - ''https://BrentOzar.com/go/audits'' AS URL, - (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* - But what if you need to run a query in every individual database? - Hop down to the @CheckUserDatabaseObjects section. - - And that's the basic idea! You can read through the rest of the - checks if you like - some more exciting stuff happens closer to the - end of the stored proc, where we start doing things like checking - the plan cache, but those aren't as cleanly commented. - - If you'd like to contribute your own check, use one of the check - formats shown above and email it to Help@BrentOzar.com. You don't - have to pick a CheckID or a link - we'll take care of that when we - test and publish the code. Thanks! - */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 93 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 93) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 93 AS CheckID , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backing Up to Same Drive Where Databases Reside' AS Finding , - 'https://BrentOzar.com/go/backup' AS URL , - CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' - + UPPER(LEFT(bmf.physical_device_name, 3)) - + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details - FROM msdb.dbo.backupmediafamily AS bmf - INNER JOIN msdb.dbo.backupset AS bs ON bmf.media_set_id = bs.media_set_id - AND bs.backup_start_date >= ( DATEADD(dd, - -14, GETDATE()) ) - /* Filter out databases that were recently restored: */ - LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name AND rh.restore_date > DATEADD(dd, -14, GETDATE()) - WHERE UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( - SELECT DISTINCT - UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) - FROM sys.master_files AS mf ) - AND rh.destination_database_name IS NULL - GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 119 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_database_encryption_keys' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 119) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, DatabaseName, URL, Details) - SELECT 119 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''TDE Certificate Not Backed Up Recently'' AS Finding, - db_name(dek.database_id) AS DatabaseName, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 202 ) - AND EXISTS ( SELECT * - FROM sys.all_columns c - WHERE c.name = 'pvt_key_last_backup_date' ) - AND EXISTS ( SELECT * - FROM msdb.INFORMATION_SCHEMA.COLUMNS c - WHERE c.TABLE_NAME = 'backupset' AND c.COLUMN_NAME = 'encryptor_thumbprint' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 202 AS CheckID, - 1 AS Priority, - ''Backup'' AS FindingsGroup, - ''Encryption Certificate Not Backed Up Recently'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - ''The certificate '' + c.name + '' is used to encrypt database backups. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c - INNER JOIN msdb.dbo.backupset bs ON c.thumbprint = bs.encryptor_thumbprint - WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 3 ) - BEGIN - IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 3) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 3 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Not Purged' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 186 ) - BEGIN - IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 186) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 186 AS CheckID , - 'msdb' , - 200 AS Priority , - 'Backup' AS FindingsGroup , - 'MSDB Backup History Purged Too Frequently' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , - ( 'Database backup history only retained back to ' - + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details - FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 178 ) - AND EXISTS (SELECT * - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 178) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 178 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Snapshot Backups Occurring' AS Finding , - 'https://BrentOzar.com/go/snaps' AS URL , - ( CAST(COUNT(*) AS VARCHAR(20)) + ' snapshot-looking backups have occurred in the last two weeks, indicating that IO may be freezing up.') AS Details - FROM msdb.dbo.backupset bs - WHERE bs.type = 'D' - AND bs.backup_size >= 50000000000 /* At least 50GB */ - AND DATEDIFF(SECOND, bs.backup_start_date, bs.backup_finish_date) <= 60 /* Backup took less than 60 seconds */ - AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()); /* In the last 2 weeks */ - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 4 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 4) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 4 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Sysadmins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.sysadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0 - AND l.name NOT LIKE 'NT SERVICE\%' - AND l.name <> 'l_certSignSmDetach'; /* Added in SQL 2016 */ - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 5 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 5) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 5 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Security Admins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , - ( 'Login [' + l.name - + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details - FROM master.sys.syslogins l - WHERE l.securityadmin = 1 - AND l.name <> SUSER_SNAME(0x01) - AND l.denylogin = 0; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 104 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 104) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] - ) - SELECT 104 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Login Can Control Server' AS [Finding] , - 'https://BrentOzar.com/go/sa' AS [URL] , - 'Login [' + pri.[name] - + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] - FROM sys.server_principals AS pri - WHERE pri.[principal_id] IN ( - SELECT p.[grantee_principal_id] - FROM sys.server_permissions AS p - WHERE p.[state] IN ( 'G', 'W' ) - AND p.[class] = 100 - AND p.[type] = 'CL' ) - AND pri.[name] NOT LIKE '##%##'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 6 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 6) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 6 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Jobs Owned By Users' AS Finding , - 'https://BrentOzar.com/go/owners' AS URL , - ( 'Job [' + j.name + '] is owned by [' - + SUSER_SNAME(j.owner_sid) - + '] - 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); - END; - - - /* --TOURSTOP06-- */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 7 ) - BEGIN - /* --TOURSTOP02-- */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 7) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 7 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Stored Procedure Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Stored procedure [master].[' - + r.SPECIFIC_SCHEMA + '].[' - + r.SPECIFIC_NAME - + '] runs automatically when SQL Server starts up. Make sure you know exactly what this stored procedure is doing, because it could pose a security risk.' ) AS Details - FROM master.INFORMATION_SCHEMA.ROUTINES r - WHERE OBJECTPROPERTY(OBJECT_ID(ROUTINE_NAME), - 'ExecIsStartup') = 1; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 10 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 10) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 10 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Resource Governor Enabled'' AS Finding, - ''https://BrentOzar.com/go/rg'' AS URL, - (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 11 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 11) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 11 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Server Triggers Enabled'' AS Finding, - ''https://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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 12 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 12) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 12 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Close Enabled' AS Finding , - 'https://BrentOzar.com/go/autoclose' AS URL , - ( 'Database [' + [name] - + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_close_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 12); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 13 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 13) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 13 AS CheckID , - [name] AS DatabaseName , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Enabled' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - ( 'Database [' + [name] - + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details - FROM sys.databases - WHERE is_auto_shrink_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 13); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 14 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 14) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 14 AS CheckID, - [name] as DatabaseName, - 50 AS Priority, - ''Reliability'' AS FindingsGroup, - ''Page Verification Not Optimal'' AS Finding, - ''https://BrentOzar.com/go/torn'' AS URL, - (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details - FROM sys.databases - WHERE page_verify_option < 2 - AND name <> ''tempdb'' - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 14) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 15 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 15) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 15 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Create Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/acs' AS URL , - ( 'Database [' + [name] - + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_create_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 15); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 16 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 16) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 16 AS CheckID , - [name] AS DatabaseName , - 110 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Update Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/aus' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_on = 0 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 16); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 17 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 17) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 17 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Stats Updated Asynchronously' AS Finding , - 'https://BrentOzar.com/go/asyncstats' AS URL , - ( 'Database [' + [name] - + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details - FROM sys.databases - WHERE is_auto_update_stats_async_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 17); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 18 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 18) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 18 AS CheckID , - [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Forced Parameterization On' AS Finding , - 'https://BrentOzar.com/go/forced' AS URL , - ( 'Database [' + [name] - + '] has forced parameterization enabled. SQL Server will aggressively reuse query execution plans even if the applications do not parameterize their queries. This can be a performance booster with some programming languages, or it may use universally bad execution plans when better alternatives are available for certain parameters.' ) AS Details - FROM sys.databases - WHERE is_parameterization_forced = 1 - AND name NOT IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 18); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 20 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 20 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Date Correlation On' AS Finding , - 'https://BrentOzar.com/go/corr' AS URL , - ( 'Database [' + [name] - + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details - FROM sys.databases - WHERE is_date_correlation_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 20); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 21 ) - BEGIN - /* --TOURSTOP04-- */ - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 21) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 21 AS CheckID, - [name] as DatabaseName, - 200 AS Priority, - ''Informational'' AS FindingsGroup, - ''Database Encrypted'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, - (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details - FROM sys.databases - WHERE is_encrypted = 1 - and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 21) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* - Believe it or not, SQL Server doesn't track the default values - for sp_configure options! We'll make our own list here. - */ - - 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; - 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 ); - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 22 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 22) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT cd.CheckID , - 200 AS Priority , - 'Non-Default Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/conf' AS URL , - ( 'This sp_configure option has been changed. Its default value is ' - + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), - '(unknown)') - + ' and it has been set to ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '.' ) AS Details - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - LEFT OUTER JOIN #ConfigurationDefaults cdUsed ON cdUsed.name = cr.name - AND cdUsed.DefaultValue = cr.value_in_use - WHERE cdUsed.name IS NULL; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 190 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Setting @MinServerMemory and @MaxServerMemory', 0, 1) WITH NOWAIT; - - SELECT @MinServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'min server memory (MB)'; - SELECT @MaxServerMemory = CAST(value_in_use as BIGINT) FROM sys.configurations WHERE name = 'max server memory (MB)'; - - IF (@MinServerMemory = @MaxServerMemory) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 190) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES - ( 190, - 200, - 'Performance', - 'Non-Dynamic Memory', - 'https://BrentOzar.com/go/memory', - 'Minimum Server Memory setting is the same as the Maximum (both set to ' + CAST(@MinServerMemory AS NVARCHAR(50)) + '). This will not allow dynamic memory. Please revise memory settings' - ); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 188 ) - BEGIN - - /* Let's set variables so that our query is still SARGable */ - - IF @Debug IN (1, 2) RAISERROR('Setting @Processors.', 0, 1) WITH NOWAIT; - - SET @Processors = (SELECT cpu_count FROM sys.dm_os_sys_info); - - IF @Debug IN (1, 2) RAISERROR('Setting @NUMANodes', 0, 1) WITH NOWAIT; - - SET @NUMANodes = (SELECT COUNT(1) - FROM sys.dm_os_performance_counters pc - WHERE pc.object_name LIKE '%Buffer Node%' - AND counter_name = 'Page life expectancy'); - /* If Cost Threshold for Parallelism is default then flag as a potential issue */ - /* If MAXDOP is default and processors > 8 or NUMA nodes > 1 then flag as potential issue */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 188) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 188 AS CheckID , - 200 AS Priority , - 'Performance' AS FindingsGroup , - cr.name AS Finding , - 'https://BrentOzar.com/go/cxpacket' AS URL , - ( 'Set to ' + CAST(cr.value_in_use AS NVARCHAR(50)) + ', its default value. Changing this sp_configure setting may reduce CXPACKET waits.') - FROM sys.configurations cr - INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name - AND cr.value_in_use = cd.DefaultValue - WHERE cr.name = 'cost threshold for parallelism' - OR (cr.name = 'max degree of parallelism' AND (@NUMANodes > 1 OR @Processors > 8)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 24 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 24) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 24 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'System Database on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) IN ( 'master', - 'model', 'msdb' ); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 25 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 25) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 25 AS CheckID , - 'tempdb' , - 20 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - CASE WHEN growth > 0 - THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) - ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) - END AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) = 'tempdb'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 26 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 26) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 26 AS CheckID , - DB_NAME(database_id) AS DatabaseName , - 20 AS Priority , - 'Reliability' AS FindingsGroup , - 'User Databases on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , - ( 'The ' + DB_NAME(database_id) - + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details - FROM sys.master_files - WHERE UPPER(LEFT(physical_name, 1)) = 'C' - AND DB_NAME(database_id) NOT IN ( 'master', - 'model', 'msdb', - 'tempdb' ) - AND DB_NAME(database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 26 ); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 27 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 27) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 'master' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Master Database' AS Finding , - 'https://BrentOzar.com/go/mastuser' AS URL , - ( 'The ' + name - + ' table in the master database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. 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; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 28 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 28) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 28 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the MSDB Database' AS Finding , - 'https://BrentOzar.com/go/msdbuser' AS URL , - ( 'The ' + name - + ' table in the msdb database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the msdb database may not be restored in the event of a disaster.' ) AS Details - FROM msdb.sys.tables - WHERE is_ms_shipped = 0 AND name NOT LIKE '%DTA_%'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 29 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 29) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 29 AS CheckID , - 'msdb' AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Tables in the Model Database' AS Finding , - 'https://BrentOzar.com/go/model' AS URL , - ( 'The ' + name - + ' table in the model database was created by end users on ' - + CAST(create_date AS VARCHAR(20)) - + '. Tables in the model database are automatically copied into all new databases.' ) AS Details - FROM model.sys.tables - WHERE is_ms_shipped = 0; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 30 ) - BEGIN - IF ( SELECT COUNT(*) - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 - ) < 7 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 30) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 30 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Not All Alerts Configured' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - END; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 59 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE enabled = 1 - AND COALESCE(has_notification, 0) = 0 - AND (job_id IS NULL OR job_id = 0x)) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 59) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 59 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Configured without Follow Up' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 96 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE message_id IN ( 823, 824, 825 ) ) - - BEGIN; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 96) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 96 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Corruption' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; - - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 61 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysalerts - WHERE severity BETWEEN 19 AND 25 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 61) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 61 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Alerts for Sev 19-25' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , - ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; - - END; - - END; - - --check for disabled alerts - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 98 ) - BEGIN - IF EXISTS ( SELECT name - FROM msdb.dbo.sysalerts - WHERE enabled = 0 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 98) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 98 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Alerts Disabled' AS Finding , - 'https://www.BrentOzar.com/go/alerts/' AS URL , - ( 'The following Alert is disabled, please review and enable if desired: ' - + name ) AS Details - FROM msdb.dbo.sysalerts - WHERE enabled = 0; - - END; - - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 31 ) - BEGIN - IF NOT EXISTS ( SELECT * - FROM msdb.dbo.sysoperators - WHERE enabled = 1 ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 31) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 31 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No Operators Configured/Enabled' AS Finding , - 'https://BrentOzar.com/go/op' AS URL , - ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; - - END; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 34 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_db_mirroring_auto_page_repair' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 34) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 34 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details - FROM (SELECT rp2.database_id, rp2.modification_time - FROM sys.dm_db_mirroring_auto_page_repair rp2 - WHERE rp2.[database_id] not in ( - SELECT db2.[database_id] - FROM sys.databases as db2 - WHERE db2.[state] = 1 - ) ) as rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 89 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_hadr_auto_page_repair' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 89) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 89 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''AlwaysOn has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details - FROM sys.dm_hadr_auto_page_repair rp - INNER JOIN master.sys.databases db ON rp.database_id = db.database_id - WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 90 ) - BEGIN - IF EXISTS ( SELECT * - FROM msdb.sys.all_objects - WHERE name = 'suspect_pages' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 90) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT - 90 AS CheckID , - db.name , - 1 AS Priority , - ''Corruption'' AS FindingsGroup , - ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details - FROM msdb.dbo.suspect_pages sp - INNER JOIN master.sys.databases db ON sp.database_id = db.database_id - WHERE sp.last_update_date >= DATEADD(dd, -30, GETDATE()) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 36 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 36) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 36 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Reads on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Reads are averaging longer than 200ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_read_ms / ( 1.0 + num_of_reads ) ) > 200 - AND num_of_reads > 100000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 37 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 37) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 37 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Slow Storage Writes on Drive ' - + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , - 'Writes are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details - FROM sys.dm_io_virtual_file_stats(NULL, NULL) - AS fs - INNER JOIN sys.master_files AS mf ON fs.database_id = mf.database_id - AND fs.[file_id] = mf.[file_id] - WHERE ( io_stall_write_ms / ( 1.0 - + num_of_writes ) ) > 100 - AND num_of_writes > 100000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 40 ) - BEGIN - IF ( SELECT COUNT(*) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - ) = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 40) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 40 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Only Has 1 Data File' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' - ); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 183 ) - - BEGIN - - IF ( SELECT COUNT (distinct [size]) - FROM tempdb.sys.database_files - WHERE type_desc = 'ROWS' - 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; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( 183 , - 'tempdb' , - 170 , - 'File Configuration' , - 'TempDB Unevenly Sized Data Files' , - 'https://BrentOzar.com/go/tempdb' , - 'TempDB data files are not configured with the same size. Unevenly sized tempdb data files will result in unevenly sized workloads.' - ); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 44 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 44) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 44 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Order Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'order hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 45 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 45) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 45 AS CheckID , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Queries Forcing Join Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , - CAST(occurrence AS VARCHAR(10)) - + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details - FROM sys.dm_exec_query_optimizer_info - WHERE counter = 'join hint' - AND occurrence > 1000; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 49 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 49) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 49 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Linked Server Configured' AS Finding , - 'https://BrentOzar.com/go/link' AS URL , - +CASE WHEN l.remote_name = 'sa' - THEN s.data_source - + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' - ELSE s.data_source - + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' - END AS Details - FROM sys.servers s - INNER JOIN sys.linked_logins l ON s.server_id = l.server_id - WHERE s.is_linked = 1; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 50 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 50) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 50 AS CheckID , - 100 AS Priority , - ''Performance'' AS FindingsGroup , - ''Max Memory Set Too High'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''SQL Server max memory is set to '' - + CAST(c.value_in_use AS VARCHAR(20)) - + '' megabytes, but the server only has '' - + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes. SQL Server may drain the system dry of memory, and under certain conditions, this can cause Windows to swap to disk.'' AS Details - FROM sys.dm_os_sys_memory m - INNER JOIN sys.configurations c ON c.name = ''max server memory (MB)'' - WHERE CAST(m.total_physical_memory_kb AS BIGINT) < ( CAST(c.value_in_use AS BIGINT) * 1024 ) 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 51 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 51) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 51 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) - + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details - FROM sys.dm_os_sys_memory m - WHERE CAST(m.available_physical_memory_kb AS BIGINT) < 262144 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 159 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 159) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT DISTINCT 159 AS CheckID , - 1 AS Priority , - ''Performance'' AS FindingsGroup , - ''Memory Dangerously Low in NUMA Nodes'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , - ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details - FROM sys.dm_os_nodes m - WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%'' 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 53 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 53) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 - 53 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Cluster Node' AS Finding , - 'https://BrentOzar.com/go/node' AS URL , - 'This is a node in a cluster.' AS Details - FROM sys.dm_os_cluster_nodes; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 55 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 55 AS CheckID , - [name] AS DatabaseName , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'Database Owner <> SA' AS Finding , - 'https://BrentOzar.com/go/owndb' AS URL , - ( 'Database name: ' + [name] + ' ' - + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details - FROM sys.databases - WHERE SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01) - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 55); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 57 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 57) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 57 AS CheckID , - 230 AS Priority , - 'Security' AS FindingsGroup , - 'SQL Agent Job Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , - ( 'Job [' + j.name - + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details - FROM msdb.dbo.sysschedules sched - JOIN msdb.dbo.sysjobschedules jsched ON sched.schedule_id = jsched.schedule_id - JOIN msdb.dbo.sysjobs j ON jsched.job_id = j.job_id - WHERE sched.freq_type = 64 - AND sched.enabled = 1; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 97 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 97) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 97 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Unusual SQL Server Edition' AS Finding , - 'https://BrentOzar.com/go/workgroup' AS URL , - ( 'This server is using ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + ', which is capped at low amounts of CPU and memory.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Data Center%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Business Intelligence%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 154 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 154) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 154 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - '32-bit SQL Server Installed' AS Finding , - 'https://BrentOzar.com/go/32bit' AS URL , - ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 62 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 62) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 62 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Performance' AS FindingsGroup , - 'Old Compatibility Level' AS Finding , - 'https://BrentOzar.com/go/compatlevel' AS URL , - ( 'Database ' + [name] - + ' is compatibility level ' - + CAST(compatibility_level AS VARCHAR(20)) - + ', which may cause unwanted results when trying to run queries that have newer T-SQL features.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 62) - AND compatibility_level <= 90; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 94 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 94) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 94 AS CheckID , - 200 AS [Priority] , - 'Monitoring' AS FindingsGroup , - 'Agent Jobs Without Failure Emails' AS Finding , - 'https://BrentOzar.com/go/alerts' AS URL , - 'The job ' + [name] - + ' has not been set up to notify an operator if it fails.' AS Details - FROM msdb.[dbo].[sysjobs] j - INNER JOIN ( SELECT DISTINCT - [job_id] - FROM [msdb].[dbo].[sysjobschedules] - WHERE next_run_date > 0 - ) s ON j.job_id = s.job_id - WHERE j.enabled = 1 - AND j.notify_email_operator_id = 0 - AND j.notify_netsend_operator_id = 0 - AND j.notify_page_operator_id = 0 - AND j.category_id <> 100; /* Exclude SSRS category */ - END; - - - IF EXISTS ( SELECT 1 - FROM sys.configurations - WHERE name = 'remote admin connections' - AND value_in_use = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 100 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 100) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 100 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Remote DAC Disabled' AS Finding , - 'https://BrentOzar.com/go/dac' AS URL , - 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.'; - END; - - - IF EXISTS ( SELECT * - FROM sys.dm_os_schedulers - WHERE is_online = 0 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 101 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 101) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 101 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'CPU Schedulers Offline' AS Finding , - 'https://BrentOzar.com/go/schedulers' AS URL , - 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 110 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_os_memory_nodes') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 110) WITH NOWAIT; - - SET @StringToExecute = 'IF EXISTS (SELECT * - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - WHERE n.node_state_desc = ''OFFLINE'') - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 110 AS CheckID , - 50 AS Priority , - ''Performance'' AS FindingGroup , - ''Memory Nodes Offline'' AS Finding , - ''https://BrentOzar.com/go/schedulers'' AS URL , - ''Due to affinity masking or licensing problems, some of the memory may not be available.'' 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF EXISTS ( SELECT * - FROM sys.databases - WHERE state > 1 ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 102 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 102) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 102 AS CheckID , - [name] , - 20 AS Priority , - 'Reliability' AS FindingGroup , - 'Unusual Database State: ' + [state_desc] AS Finding , - 'https://BrentOzar.com/go/repair' AS URL , - 'This database may not be online.' - FROM sys.databases - WHERE state > 1; - END; - - IF EXISTS ( SELECT * - FROM master.sys.extended_procedures ) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 105 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 105) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 105 AS CheckID , - 'master' , - 200 AS Priority , - 'Reliability' AS FindingGroup , - 'Extended Stored Procedures in Master' AS Finding , - 'https://BrentOzar.com/go/clr' AS URL , - 'The [' + name - + '] extended stored procedure is in the master database. CLR may be in use, and the master database now needs to be part of your backup/recovery planning.' - FROM master.sys.extended_procedures; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 107 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 107) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 107 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: ' + wait_type AS Finding , - 'https://BrentOzar.com/go/poison/#' + wait_type AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') - GROUP BY wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 121 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 121) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 121 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: Serializable Locking' AS Finding , - 'https://BrentOzar.com/go/serializable' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - - IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 162 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 162 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingGroup , - 'Poison Wait Detected: CMEMTHREAD & NUMA' AS Finding , - 'https://BrentOzar.com/go/poison' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' - FROM sys.dm_os_nodes n - INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' - WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 - AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') - GROUP BY w.wait_type - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; - END; - - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 111 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 111) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - DatabaseName , - URL , - Details - ) - SELECT 111 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingGroup , - 'Possibly Broken Log Shipping' AS Finding , - d.[name] , - 'https://BrentOzar.com/go/shipping' AS URL , - d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' - FROM [master].sys.databases d - INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id - AND dm.mirroring_role IS NULL - WHERE ( d.[state] = 1 - OR (d.[state] = 0 AND d.[is_in_standby] = 1) ) - AND NOT EXISTS(SELECT * FROM msdb.dbo.restorehistory rh - INNER JOIN msdb.dbo.backupset bs ON rh.backup_set_id = bs.backup_set_id - WHERE d.[name] COLLATE SQL_Latin1_General_CP1_CI_AS = rh.destination_database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND rh.restore_date >= DATEADD(dd, -2, GETDATE())); - - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 112 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'change_tracking_databases') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 112) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 112 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Change Tracking Enabled'' AS Finding, - ''https://BrentOzar.com/go/tracking'' AS URL, - ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 116 ) - AND EXISTS (SELECT * FROM msdb.sys.all_columns WHERE name = 'compressed_backup_size') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 116) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 116 AS CheckID , - 200 AS Priority , - ''Informational'' AS FindingGroup , - ''Backup Compression Default Off'' AS Finding , - ''https://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.'' - 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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 117 ) - AND EXISTS (SELECT * FROM master.sys.all_objects WHERE name = 'dm_exec_query_resource_semaphores') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 117) WITH NOWAIT; - - SET @StringToExecute = 'IF 0 < (SELECT SUM([forced_grant_count]) FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL) - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 117 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Memory Pressure Affecting Queries'' AS Finding, - ''https://BrentOzar.com/go/grants'' AS URL, - CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' - FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT 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.'; - - EXECUTE(@StringToExecute); - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 124 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 124) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 124, 150, 'Performance', 'Deadlocks Happening Daily', 'https://BrentOzar.com/go/deadlocks', - CAST(p.cntr_value AS NVARCHAR(100)) + ' deadlocks recorded since startup. To find them, run sp_BlitzLock.' AS Details - FROM sys.dm_os_performance_counters p - INNER JOIN sys.databases d ON d.name = 'tempdb' - WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' - AND RTRIM(p.instance_name) = '_Total' - AND p.cntr_value > 0 - AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; - END; - - - IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 125 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 125) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://BrentOzar.com/askbrent/plan-cache-erased-recently/', - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' - FROM sys.dm_exec_query_stats WITH (NOLOCK) - ORDER BY creation_time; - END; - - IF EXISTS (SELECT * FROM sys.configurations WHERE name = 'priority boost' AND (value = 1 OR value_in_use = 1)) - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 126 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 126) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://BrentOzar.com/go/priorityboost/', - 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.'); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 128 ) - BEGIN - - IF (@ProductVersionMajor = 12 AND @ProductVersionMinor < 5000) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor < 6020) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 9 /*AND @ProductVersionMinor <= 5000*/) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 128) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://BrentOzar.com/go/unsupported', - 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + - CASE WHEN @ProductVersionMajor > 9 THEN - CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' - ELSE ' is no longer support by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); - END; - - END; - - /* Reliability - Dangerous Build of SQL Server (Corruption) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 129 ) - BEGIN - IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2342) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 129) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(129, 20, 'Reliability', 'Dangerous Build of SQL Server (Corruption)', 'http://sqlperformance.com/2014/06/sql-indexes/hotfix-sql-2012-rebuilds', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; - - END; - - /* Reliability - Dangerous Build of SQL Server (Security) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 157 ) - BEGIN - IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4000 AND @ProductVersionMinor <= 4017) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor >= 4251 AND @ProductVersionMinor <= 4319) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3129) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3300 AND @ProductVersionMinor <= 3447) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2000 AND @ProductVersionMinor <= 2253) OR - (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 2300 AND @ProductVersionMinor <= 2370) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 157) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(157, 20, 'Reliability', 'Dangerous Build of SQL Server (Security)', 'https://technet.microsoft.com/en-us/library/security/MS14-044', - 'There are dangerous known bugs with version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + '. Check the URL for details and apply the right service pack or hotfix.'); - END; - - END; - - /* Check if SQL 2016 Standard Edition but not SP1 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 189 ) - BEGIN - IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 189) WITH NOWAIT; - - INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(189, 100, 'Features', 'Missing Features', 'https://blogs.msdn.microsoft.com/sqlreleaseservices/sql-server-2016-service-pack-1-sp1-released/', - 'SQL 2016 Standard Edition is being used but not Service Pack 1. Check the URL for a list of Enterprise Features that are included in Standard Edition as of SP1.'); - END; - - END; - - /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 145 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 145) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 145 AS CheckID, - 10 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) - OR SUM(mem.pages_kb / 1024.0) > 250000 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - In-Memory OLTP (Hekaton) In Use */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 146 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_db_xtp_table_memory_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 146) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 146 AS CheckID, - 200 AS Priority, - ''Performance'' AS FindingsGroup, - ''In-Memory OLTP (Hekaton) In Use'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details - FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' - WHERE c.name = ''max server memory (MB)'' - GROUP BY c.value_in_use - HAVING SUM(mem.pages_kb / 1024.0) > 10 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.'; - - EXECUTE(@StringToExecute); - END; - - /* In-Memory OLTP (Hekaton) - Transaction Errors */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 147 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_xtp_transaction_stats' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 147) WITH NOWAIT - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 147 AS CheckID, - 100 AS Priority, - ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, - ''Transaction Errors'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details - FROM sys.dm_xtp_transaction_stats - WHERE validation_failures <> 0 - OR dependencies_failed <> 0 - OR write_conflicts <> 0 - OR unique_constraint_violations <> 0 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.'; - - EXECUTE(@StringToExecute); - END; - - - - /* Reliability - Database Files on Network File Shares */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 148 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 148) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 148 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files on Network File Shares' AS Finding , - 'https://BrentOzar.com/go/nas' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE '\\%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 148); - END; - - /* Reliability - Database Files Stored in Azure */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 149 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 149) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 149 AS CheckID , - d.[name] AS DatabaseName , - 170 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Files Stored in Azure' AS Finding , - 'https://BrentOzar.com/go/azurefiles' AS URL , - ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details - FROM sys.databases d - INNER JOIN sys.master_files mf ON d.database_id = mf.database_id - WHERE mf.physical_name LIKE 'http://%' - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 149); - END; - - - /* Reliability - Errors Logged Recently in the Default Trace */ - - /* First, let's check that there aren't any issues with the trace files */ - BEGIN TRY - - INSERT INTO #fnTraceGettable - ( TextData , - DatabaseName , - EventClass , - Severity , - StartTime , - EndTime , - Duration , - NTUserName , - NTDomainName , - HostName , - ApplicationName , - LoginName , - DBUserName - ) - SELECT TOP 20000 - CONVERT(NVARCHAR(4000),t.TextData) , - t.DatabaseName , - t.EventClass , - t.Severity , - t.StartTime , - t.EndTime , - t.Duration , - t.NTUserName , - t.NTDomainName , - t.HostName , - t.ApplicationName , - t.LoginName , - t.DBUserName - FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t - WHERE - ( - t.EventClass = 22 - AND t.Severity >= 17 - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - ) - OR - ( - t.EventClass IN (92, 93) - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - AND t.Duration > 15000000 - ) - OR - ( - t.EventClass IN (94, 95, 116) - ) - - SET @TraceFileIssue = 0 - - END TRY - BEGIN CATCH - - SET @TraceFileIssue = 1 - - END CATCH - - IF @TraceFileIssue = 1 - BEGIN - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 199 ) - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - '199' AS CheckID , - '' AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'There Is An Error With The Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - 'Somebody has been messing with your trace files. Check the files are present at ' + @base_tracefilename AS Details - END - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 150 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 150) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 150 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Errors Logged Recently in the Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , - CAST(t.TextData AS NVARCHAR(4000)) AS Details - FROM #fnTraceGettable t - WHERE t.EventClass = 22 - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.Severity >= 17 - --AND t.StartTime > DATEADD(dd, -30, GETDATE()); - END; - - - /* Performance - File Growths Slow */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 151 ) - AND @base_tracefilename IS NOT NULL - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 151) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT 151 AS CheckID , - t.DatabaseName, - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'File Growths Slow' AS Finding , - 'https://BrentOzar.com/go/filegrowth' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting file autogrowth to a smaller increment.' AS Details - FROM #fnTraceGettable t - WHERE t.EventClass IN (92, 93) - /* Removed these as they're unnecessary, we filter this when inserting data into #fnTraceGettable */ - --AND t.StartTime > DATEADD(dd, -30, GETDATE()) - --AND t.Duration > 15000000 - GROUP BY t.DatabaseName - HAVING COUNT(*) > 1; - END; - - - /* Performance - Many Plans for One Query */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 160 ) - AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 160) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 160 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Many Plans for One Query'' AS Finding, - ''https://BrentOzar.com/go/parameterization'' AS URL, - CAST(COUNT(DISTINCT plan_handle) AS NVARCHAR(50)) + '' plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa - WHERE pa.attribute = ''dbid'' - GROUP BY qs.query_hash, pa.value - HAVING COUNT(DISTINCT plan_handle) > 50 - ORDER BY COUNT(DISTINCT plan_handle) DESC 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - High Number of Cached Plans */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 161 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 161) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 161 AS CheckID, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''High Number of Cached Plans'' AS Finding, - ''https://BrentOzar.com/go/planlimits'' AS URL, - ''Your server configuration is limited to '' + CAST(ht.buckets_count * 4 AS VARCHAR(20)) + '' '' + ht.name + '', and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details - FROM sys.dm_os_memory_cache_hash_tables ht - INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type - where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) - AND cc.entries_count >= (3 * ht.buckets_count) 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.'; - - EXECUTE(@StringToExecute); - END; - - - /* Performance - Too Much Free Memory */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 165 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 165) WITH NOWAIT; - - INSERT INTO #BlitzResults - (CheckID, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://BrentOzar.com/go/freememory', - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool, which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - END; - - - /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 155 ) - AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 155) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 155 AS CheckID , - 0 AS Priority , - 'Outdated sp_Blitz' AS FindingsGroup , - 'sp_Blitz is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details; - END; - - - /* Populate a list of database defaults. I'm doing this kind of oddly - - it reads like a lot of work, but this way it compiles & runs on all - versions of SQL Server. - */ - - IF @Debug IN (1, 2) RAISERROR('Generating database defaults.', 0, 1) WITH NOWAIT; - - INSERT INTO #DatabaseDefaults - SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_read_committed_snapshot_on', 0, 133, 210, 'Read Committed Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); - /* Not alerting for this since we actually want it and we have a separate check for it: - INSERT INTO #DatabaseDefaults - SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); - */ - INSERT INTO #DatabaseDefaults - SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); - - DECLARE DatabaseDefaultsLoop CURSOR FOR - SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details - FROM #DatabaseDefaults; - - OPEN DatabaseDefaultsLoop; - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - WHILE @@FETCH_STATUS = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, @CurrentCheckID) WITH NOWAIT; - - /* Target Recovery Time (142) can be either 0 or 60 due to a number of bugs */ - IF @CurrentCheckID = 142 - 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.[' + @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.[' + @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.'; - - EXEC (@StringToExecute); - - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; - END; - - CLOSE DatabaseDefaultsLoop; - DEALLOCATE DatabaseDefaultsLoop; - - -/*This checks to see if Agent is Offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 167 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 167) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 167 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Agent is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Server Agent%' - AND CAST(SERVERPROPERTY('Edition') AS VARCHAR(1000)) NOT LIKE '%xpress%'; - - END; - END; - -/*This checks to see if the Full Text thingy is offline*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 168 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 168 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; - - END; - END; - -/*This checks which service account SQL Server is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 169 ) - - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 169) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 169 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server%' - AND [servicename] NOT LIKE 'SQL Server Agent%'; - - END; - END; - -/*This checks which service account SQL Agent is running as.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 170 ) - - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 170) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 170 AS [CheckID] , - 250 AS [Priority] , - 'Informational' AS [FindingsGroup] , - 'SQL Server Agent is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' - ) AS [Details] - FROM - [sys].[dm_server_services] - WHERE [service_account] LIKE 'NT Service%' - AND [servicename] LIKE 'SQL Server Agent%'; - - END; - END; - -/*This counts memory dumps and gives min and max date of in view*/ -IF @ProductVersionMajor >= 10 - AND NOT (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 4297) /* Skip due to crash bug: https://support.microsoft.com/en-us/help/2908087 */ - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 171 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_memory_dumps' ) - BEGIN - IF 5 <= (SELECT COUNT(*) FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 171) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 171 AS [CheckID] , - 20 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Memory Dumps Have Occurred' AS [Finding] , - 'https://BrentOzar.com/go/dump' AS [URL] , - ( 'That ain''t good. I''ve had ' + - CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + - CAST(CAST(MIN([creation_time]) AS DATETIME) AS VARCHAR(100)) + - ' and ' + - CAST(CAST(MAX([creation_time]) AS DATETIME) AS VARCHAR(100)) + - '!' - ) AS [Details] - FROM - [sys].[dm_server_memory_dumps] - WHERE [creation_time] >= DATEADD(year, -1, GETDATE()); - - END; - END; - END; - -/*Checks to see if you're on Developer or Evaluation*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 173 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 173) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 173 AS [CheckID] , - 200 AS [Priority] , - 'Licensing' AS [FindingsGroup] , - 'Non-Production License' AS [Finding] , - 'https://BrentOzar.com/go/licensing' AS [URL] , - ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + - CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + - ' the good folks at Microsoft might get upset with you. Better start counting those cores.' - ) AS [Details] - WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Developer%' - OR CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%Evaluation%'; - - END; - -/*Checks to see if Buffer Pool Extensions are in use*/ - IF @ProductVersionMajor >= 12 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 174 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 174) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 174 AS [CheckID] , - 200 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Buffer Pool Extensions Enabled' AS [Finding] , - 'https://BrentOzar.com/go/bpe' AS [URL] , - ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + - [path] + - '. It''s currently ' + - CASE WHEN [current_size_in_kb] / 1024. / 1024. > 0 - THEN CAST([current_size_in_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([current_size_in_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + - '. Did you know that BPEs only provide single threaded access 8KB (one page) at a time?' - ) AS [Details] - FROM sys.dm_os_buffer_pool_extension_configuration - WHERE [state_description] <> 'BUFFER POOL EXTENSION DISABLED'; - - END; - -/*Check for too many tempdb files*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 175 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 175) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 175 AS CheckID , - 'TempDB' AS DatabaseName , - 170 AS Priority , - 'File Configuration' AS FindingsGroup , - 'TempDB Has >16 Data Files' AS Finding , - 'https://BrentOzar.com/go/tempdb' AS URL , - 'Woah, Nelly! TempDB has ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + '. Did you forget to terminate a loop somewhere?' AS Details - FROM sys.[master_files] AS [mf] - WHERE [mf].[database_id] = 2 AND [mf].[type] = 0 - HAVING COUNT_BIG(*) > 16; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 176 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_xe_sessions' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 176) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 176 AS CheckID , - '' AS DatabaseName , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Extended Events Hyperextension' AS Finding , - 'https://BrentOzar.com/go/xe' AS URL , - 'Hey big spender, you have ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + ' Extended Events sessions running. You sure you meant to do that?' AS Details - FROM sys.dm_xe_sessions - WHERE [name] NOT IN - ( 'AlwaysOn_health', 'system_health', 'telemetry_xevents', 'sp_server_diagnostics', 'hkenginexesession' ) - AND name NOT LIKE '%$A%' - HAVING COUNT_BIG(*) >= 2; - END; - END; - - /*Harmful startup parameter*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 177 ) - BEGIN - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_registry' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 177) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 177 AS CheckID , - '' AS DatabaseName , - 5 AS Priority , - 'Monitoring' AS FindingsGroup , - 'Disabled Internal Monitoring Features' AS Finding , - 'https://msdn.microsoft.com/en-us/library/ms190737.aspx' AS URL , - 'You have -x as a startup parameter. You should head to the URL and read more about what it does to your system.' AS Details - FROM - [sys].[dm_server_registry] AS [dsr] - WHERE - [dsr].[registry_key] LIKE N'%MSSQLServer\Parameters' - AND [dsr].[value_data] = '-x';; - END; - END; - - - /* Reliability - Dangerous Third Party Modules - 179 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 179 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 179) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 179 AS [CheckID] , - 5 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'Dangerous Third Party Modules' AS [Finding] , - 'https://support.microsoft.com/en-us/kb/2033238' AS [URL] , - ( COALESCE(company, '') + ' - ' + COALESCE(description, '') + ' - ' + COALESCE(name, '') + ' - suspected dangerous third party module is installed.') AS [Details] - FROM sys.dm_os_loaded_modules - WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') /* McAfee VirusScan Enterprise */ - OR UPPER(name) LIKE UPPER('%\HIPI.DLL') OR UPPER(name) LIKE UPPER('%\HcSQL.dll') OR UPPER(name) LIKE UPPER('%\HcApi.dll') OR UPPER(name) LIKE UPPER('%\HcThe.dll') /* McAfee Host Intrusion */ - OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') /* Sophos AV */ - OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL'); /* OSISoft PI data access */ - - END; - - /*Find shrink database tasks*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 180 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 180) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , [id] -- ID required to link maintenace plan with jobs and jobhistory (sp_Blitz Issue #776) - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ) - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 180 AS [CheckID] , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS [FindingsGroup] , - 'Shrink Database Step In Maintenance Plan' AS [Finding] , - 'https://BrentOzar.com/go/autoshrink' AS [URL] , - 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS [Details] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - join msdb.dbo.sysmaintplan_subplans as sms - on mps.id = sms.plan_id - JOIN msdb.dbo.sysjobs j - on sms.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobsteps AS step - ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE [c].[value]('(@dts:ObjectName)', 'VARCHAR(128)') = 'Shrink Database Task'; - - END; - - - /*Find repetitive maintenance tasks*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 181 ) - AND CONVERT(VARCHAR(128), SERVERPROPERTY ('productversion')) LIKE '1%' /* Only run on 2008+ */ - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 181) WITH NOWAIT; - - WITH XMLNAMESPACES ('www.microsoft.com/SqlServer/Dts' AS [dts]) - ,[maintenance_plan_steps] AS ( - SELECT [name] - , CAST(CAST([packagedata] AS VARBINARY(MAX)) AS XML) AS [maintenance_plan_xml] - FROM [msdb].[dbo].[sysssispackages] - WHERE [packagetype] = 6 - ), [maintenance_plan_table] AS ( - SELECT [mps].[name] - ,[c].[value]('(@dts:ObjectName)', 'NVARCHAR(128)') AS [step_name] - FROM [maintenance_plan_steps] [mps] - CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - ), [mp_steps_pretty] AS (SELECT DISTINCT [m1].[name] , - STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] - FOR XML PATH(N'')), 1, 2, N'') AS [maintenance_plan_steps] - FROM [maintenance_plan_table] AS [m1]) - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 181 AS [CheckID] , - 100 AS [Priority] , - 'Performance' AS [FindingsGroup] , - 'Repetitive Steps In Maintenance Plans' AS [Finding] , - 'https://ola.hallengren.com/' AS [URL] , - 'The maintenance plan ' + [m].[name] + ' is doing repetitive work on indexes and statistics. Perhaps it''s time to try something more modern?' AS [Details] - FROM [mp_steps_pretty] m - WHERE m.[maintenance_plan_steps] LIKE '%Rebuild%Reorganize%' - OR m.[maintenance_plan_steps] LIKE '%Rebuild%Update%'; - - END; - - - /* Reliability - No Failover Cluster Nodes Available - 184 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '10%' - AND CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) NOT LIKE '9%' - BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT TOP 1 - 184 AS CheckID , - 20 AS Priority , - ''Reliability'' AS FindingsGroup , - ''No Failover Cluster Nodes Available'' AS Finding , - ''https://BrentOzar.com/go/node'' AS URL , - ''There are no failover cluster nodes available if the active node fails'' AS Details - FROM ( - SELECT SUM(CASE WHEN [status] = 0 AND [is_current_owner] = 0 THEN 1 ELSE 0 END) AS [available_nodes] - FROM sys.dm_os_cluster_nodes - ) a - WHERE [available_nodes] < 1 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.'; - - EXECUTE(@StringToExecute); - END; - - /* Reliability - TempDB File Error */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 191 ) - AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> (SELECT COUNT(*) FROM tempdb.sys.database_files) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 191) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 191 AS [CheckID] , - 50 AS [Priority] , - 'Reliability' AS [FindingsGroup] , - 'TempDB File Error' AS [Finding] , - 'https://BrentOzar.com/go/tempdboops' AS [URL] , - 'Mismatch between the number of TempDB files in sys.master_files versus tempdb.sys.database_files' AS [Details]; - END; - -/*Perf - Odd number of cores in a socket*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND CheckID = 198 ) - AND EXISTS ( SELECT 1 - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 198) WITH NOWAIT - - INSERT INTO #BlitzResults - ( - CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details - ) - SELECT 198 AS CheckID, - NULL AS DatabaseName, - 10 AS Priority, - 'Performance' AS FindingsGroup, - 'CPU w/Odd Number of Cores' AS Finding, - 'https://BrentOzar.com/go/oddity' AS URL, - 'Node ' + CONVERT(VARCHAR(10), parent_node_id) + ' has ' + CONVERT(VARCHAR(10), COUNT(cpu_id)) - + CASE WHEN COUNT(cpu_id) = 1 THEN ' core assigned to it. This is a really bad NUMA configuration.' - ELSE ' cores assigned to it. This is a really bad NUMA configuration.' - END AS Details - FROM sys.dm_os_schedulers - WHERE is_online = 1 - AND scheduler_id < 255 - AND parent_node_id < 64 - GROUP BY parent_node_id, - is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; - - END; - -/*Begin: checking default trace for odd DBCC activity*/ - - --Grab relevant event data - IF @TraceFileIssue = 0 - BEGIN - SELECT UPPER( - REPLACE( - SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, - ISNULL( - NULLIF( - CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), - 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. - , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ) - , '') --This replaces any optional WITH clause to a DBCC command, like tableresults. - ) AS [dbcc_event_trunc_upper], - UPPER( - REPLACE( - CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), - ISNULL( - NULLIF( - CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ), '')) AS [dbcc_event_full_upper], - MIN(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS min_start_time, - MAX(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS max_start_time, - t.NTUserName AS [nt_user_name], - t.NTDomainName AS [nt_domain_name], - t.HostName AS [host_name], - t.ApplicationName AS [application_name], - t.LoginName [login_name], - t.DBUserName AS [db_user_name] - INTO #dbcc_events_from_trace - FROM #fnTraceGettable AS t - WHERE t.EventClass = 116 - OPTION(RECOMPILE) - END; - - /*Overall count of DBCC events excluding silly stuff*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 203 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 199) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 203 AS CheckID , - 50 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'Overall Events' AS Finding , - '' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This does not include CHECKDB and other usually benign DBCC events.' - AS Details - FROM #dbcc_events_from_trace d - /* This WHERE clause below looks horrible, but it's because users can run stuff like - DBCC LOGINFO - with lots of spaces (or carriage returns, or comments) in between the DBCC and the - command they're trying to run. See Github issues 1062, 1074, 1075. - */ - WHERE d.dbcc_event_full_upper NOT LIKE '%DBCC%ADDINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKALLOC%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCATALOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCONSTRAINTS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKDB%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKFILEGROUP%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKIDENT%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKPRIMARYFILE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CLEANTABLE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%DBINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%ERRORLOG%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INCREMENTINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%INPUTBUFFER%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%LOGINFO%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%OPENTRAN%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SETINSTANCE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOWFILESTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SHOW_STATISTICS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%NETSTATS%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%SQLPERF%LOGSPACE%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEON%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEOFF%' - AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACESTATUS%' - AND d.application_name NOT LIKE 'Critical Care(R) Collector' - AND d.application_name NOT LIKE '%Red Gate Software Ltd SQL Prompt%' - AND d.application_name NOT LIKE '%Spotlight Diagnostic Server%' - AND d.application_name NOT LIKE '%SQL Diagnostic Manager%' - AND d.application_name NOT LIKE '%Sentry%' - - - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running drop clean buffers*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 207 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 200) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 207 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC DROPCLEANBUFFERS Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. If this is a production box, know that you''re clearing all data out of memory when this happens. What kind of monster would do that?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC DROPCLEANBUFFERS' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone running free proc cache*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 208 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 201) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 208 AS CheckID , - 10 AS Priority , - 'DBCC Events' AS FindingsGroup , - 'DBCC FREEPROCCACHE Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. This has bad idea jeans written all over its butt, like most other bad idea jeans.' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC FREEPROCCACHE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone clearing wait stats*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 205 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 205 AS CheckID , - 50 AS Priority , - 'Performance' AS FindingsGroup , - 'Wait Stats Cleared Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. Why are you clearing wait stats? What are you hiding?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_full_upper = N'DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR)' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - /*Check for someone writing to pages. Yeah, right?*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 209 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT 209 AS CheckID , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'DBCC WRITEPAGE Used Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying to fix corruption, or cause corruption?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper = N'DBCC WRITEPAGE' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 210 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 204) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT 210 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'DBCC SHRINK% Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying cause bad performance on purpose?' - AS Details - FROM #dbcc_events_from_trace d - WHERE d.dbcc_event_trunc_upper LIKE N'DBCC SHRINK%' - GROUP BY COALESCE(d.nt_user_name, d.login_name) - HAVING COUNT(*) > 0; - - END; - - -/*End: checking default trace for odd DBCC activity*/ - - /*Begin check for autoshrink events*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 206 ) - AND @TraceFileIssue = 0 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT 206 AS CheckID , - 10 AS Priority , - 'Performance' AS FindingsGroup , - 'Auto-Shrink Ran Recently' AS Finding , - '' AS URL , - N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' - + CONVERT(NVARCHAR(10), COUNT(*)) - + N' auto shrink events between ' - + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) - + ' that lasted on average ' - + CONVERT(NVARCHAR(10), AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime))) - + ' seconds.' AS Details - FROM #fnTraceGettable AS t - WHERE t.EventClass IN (94, 95) - GROUP BY t.DatabaseName - HAVING AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime)) > 5; - - END; - - - IF @CheckUserDatabaseObjects = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Starting @CheckUserDatabaseObjects section.', 0, 1) WITH NOWAIT - - /* - But what if you need to run a query in every individual database? - Check out CheckID 99 below. Yes, it uses sp_MSforeachdb, and no, - we're not happy about that. sp_MSforeachdb is known to have a lot - of issues, like skipping databases sometimes. However, this is the - only built-in option that we have. If you're writing your own code - for database maintenance, consider Aaron Bertrand's alternative: - http://www.mssqltips.com/sqlservertip/2201/making-a-more-reliable-and-flexible-spmsforeachdb/ - We don't include that as part of sp_Blitz, of course, because - copying and distributing copyrighted code from others without their - written permission isn't a good idea. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 99 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; 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://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 - databases. We're not checking #SkipChecks here for each database to - see if we should run the check in this database. That means we may - still run a skipped check if it involves sp_MSforeachdb. We just - don't output those results in the last step. - */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 163 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') - BEGIN - /* --TOURSTOP03-- */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 163) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 163, - ''?'', - 200, - ''Performance'', - ''Query Store Disabled'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; - END; - - - IF @ProductVersionMajor >= 13 AND @ProductVersionMinor < 2149 --CU1 has the fix in it - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 182 ) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 182) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 - 182, - ''Server'', - 20, - ''Reliability'', - ''Query Store Cleanup Disabled'', - ''https://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);'; - - 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 41 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 41) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 41, - ''?'', - 170, - ''File Configuration'', - ''Multiple Log Files on One Drive'', - ''https://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 ''?'' <> ''[tempdb]'' - GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 42 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 42) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 42, - ''?'', - 170, - ''File Configuration'', - ''Uneven File Growth Settings in One Filegroup'', - ''https://BrentOzar.com/go/grow'', - (''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'' - GROUP BY data_space_id - HAVING COUNT(DISTINCT growth) > 1 OR COUNT(DISTINCT is_percent_growth) > 1 OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 82 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 82) WITH NOWAIT; - - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 82 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to percent'', - ''https://BrentOzar.com/go/percentgrowth'' AS URL, - ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CAST((CONVERT(BIGINT, f.size * 8) / 1000000) AS NVARCHAR(10)) + '' 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);'; - END; - - - - /* addition by Henrik Staun Poulsen, Stovi Software */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 158 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 158) WITH NOWAIT; - - EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, Details) - SELECT DISTINCT 158 AS CheckID, - ''?'' as DatabaseName, - 170 AS Priority, - ''File Configuration'' AS FindingsGroup, - ''File growth set to 1MB'', - ''https://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((f.size * 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);'; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 33 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 33) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 33, - db_name(), - 200, - ''Licensing'', - ''Enterprise Edition Features In Use'', - ''https://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);'; - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 19 ) - BEGIN - /* Method 1: Check sys.databases parameters */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 19) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - - SELECT 19 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Replication In Use' AS Finding , - 'https://BrentOzar.com/go/repl' AS URL , - ( 'Database [' + [name] - + '] is a replication publisher, subscriber, or distributor.' ) AS Details - FROM sys.databases - WHERE name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 19) - AND is_published = 1 - OR is_subscribed = 1 - OR is_merge_published = 1 - OR is_distributor = 1; - - /* Method B: check subscribers for MSreplication_objects tables */ - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 19, - db_name(), - 200, - ''Informational'', - ''Replication In Use'', - ''https://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)'; - - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 32 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 32) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 32, - ''?'', - 150, - ''Performance'', - ''Triggers on Tables'', - ''https://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'' - HAVING SUM(1) > 0 OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 38 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 38) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 38, - ''?'', - 110, - ''Performance'', - ''Active Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that are being actively queried.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NOT NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 164 ) - AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'fn_validate_plan_guide') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 164) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 164, - ''?'', - 20, - ''Reliability'', - ''Plan Guides Failing'', - ''https://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)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 39 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 39) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 39, - ''?'', - 150, - ''Performance'', - ''Inactive Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that have not been queried since the last restart. These may be backup tables carelessly left behind.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 46 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 46) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 46, - ''?'', - 150, - ''Performance'', - ''Leftover Fake Indexes From Wizards'', - ''https://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);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 47 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 47) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 47, - ''?'', - 100, - ''Performance'', - ''Indexes Disabled'', - ''https://BrentOzar.com/go/ixoff'', - (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1 OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 48 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 48) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 48, - ''?'', - 150, - ''Performance'', - ''Foreign Keys Not Trusted'', - ''https://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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 56 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 56) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 56, - ''?'', - 150, - ''Performance'', - ''Check Constraint Not Trusted'', - ''https://BrentOzar.com/go/trust'', - (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') - from [?].sys.check_constraints 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 OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 95 ) - BEGIN - IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' - AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 95) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 95 AS CheckID, - ''?'' as DatabaseName, - 110 AS Priority, - ''Performance'' AS FindingsGroup, - ''Plan Guides Enabled'' AS Finding, - ''https://BrentOzar.com/go/guides'' AS URL, - (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details - FROM [?].sys.plan_guides WHERE is_disabled = 0 OPTION (RECOMPILE);'; - END; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 60 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 60) WITH NOWAIT; - - EXEC sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 60 AS CheckID, - ''?'' as DatabaseName, - 100 AS Priority, - ''Performance'' AS FindingsGroup, - ''Fill Factor Changed'', - ''https://BrentOzar.com/go/fillfactor'' AS URL, - ''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' objects with fill factor = '' + CAST(fill_factor AS NVARCHAR(5)) + ''%. This can cause memory and storage performance problems, but may also prevent page splits.'' - FROM [?].sys.indexes - WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 - GROUP BY fill_factor OPTION (RECOMPILE);'; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 78 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 78) WITH NOWAIT; - - EXECUTE master.sys.sp_MSforeachdb 'USE [?]; - INSERT INTO #Recompile - SELECT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA - FROM sys.sql_modules AS SM - LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() - LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' - LEFT OUTER JOIN INFORMATION_SCHEMA.ROUTINES AS ISR on ISR.Routine_Name = SO.name AND ISR.SPECIFIC_CATALOG = DB_Name() - WHERE SM.is_recompiled=1 OPTION (RECOMPILE); /* oh the rich irony of recompile here */ - '; - INSERT INTO #BlitzResults - (Priority, - FindingsGroup, - Finding, - DatabaseName, - URL, - Details, - CheckID) - SELECT [Priority] = '100', - FindingsGroup = 'Performance', - Finding = 'Stored Procedure WITH RECOMPILE', - DatabaseName = DBName, - URL = 'https://BrentOzar.com/go/recompile', - Details = '[' + DBName + '].[' + SPSchema + '].[' + ProcName + '] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.', - CheckID = '78' - FROM #Recompile AS TR WHERE ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%'; - DROP TABLE #Recompile; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 86 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 86) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://BrentOzar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM [?].dbo.sysmembers m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; - END; - - - /*Check for non-aligned indexes in partioned databases*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 72 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 72) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - insert into #partdb(dbname, objectname, type_desc) - SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc - FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id - JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id - LEFT OUTER JOIN sys.dm_db_index_usage_stats AS s ON i.object_id = s.object_id AND i.index_id = s.index_id AND s.database_id = DB_ID() - WHERE o.type = ''u'' - -- Clustered and Non-Clustered indexes - AND i.type IN (1, 2) - AND o.object_id in - ( - SELECT a.object_id from - (SELECT ob.object_id, ds.type_desc from sys.objects ob JOIN sys.indexes ind on ind.object_id = ob.object_id join sys.data_spaces ds on ds.data_space_id = ind.data_space_id - GROUP BY ob.object_id, ds.type_desc ) a group by a.object_id having COUNT (*) > 1 - ) OPTION (RECOMPILE);'; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT DISTINCT - 72 AS CheckID , - dbname AS DatabaseName , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'The partitioned database ' + dbname - + ' may have non-aligned indexes' AS Finding , - 'https://BrentOzar.com/go/aligned' AS URL , - 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details - FROM #partdb - WHERE dbname IS NOT NULL - AND dbname NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 72); - DROP TABLE #partdb; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 113 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 113) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 113, - ''?'', - 50, - ''Reliability'', - ''Full Text Indexes Not Updating'', - ''https://BrentOzar.com/go/fulltext'', - (''At least one full text index in this database has not been crawled in the last week.'') - from [?].sys.fulltext_indexes i WHERE change_tracking_state_desc <> ''AUTO'' AND i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE()) OPTION (RECOMPILE);'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 115 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 115) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 115, - ''?'', - 110, - ''Performance'', - ''Parallelism Rocket Surgery'', - ''https://BrentOzar.com/go/makeparallel'', - (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') - from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'' OPTION (RECOMPILE);'; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 122 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 122) WITH NOWAIT; - - /* SQL Server 2012 and newer uses temporary stats for AlwaysOn Availability Groups, and those show up as user-created */ - IF EXISTS (SELECT * - FROM sys.all_columns c - INNER JOIN sys.all_objects o ON c.object_id = o.object_id - WHERE c.name = 'is_temporary' AND o.name = 'stats') - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT TOP 1 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - - ELSE - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT 122, - ''?'', - 200, - ''Performance'', - ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', - (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') - from [?].sys.stats WHERE user_created = 1 - HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - - - END; /* IF NOT EXISTS ( SELECT 1 */ - - - /*Check for high VLF count: this will omit any database snapshots*/ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 69 ) - BEGIN - IF @ProductVersionMajor >= 11 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo2012 - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo2012 - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo2012;'; - DROP TABLE #LogInfo2012; - END; - ELSE - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (pre-2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo - EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; - IF @@ROWCOUNT > 999 - BEGIN - INSERT INTO #BlitzResults - ( CheckID - ,DatabaseName - ,Priority - ,FindingsGroup - ,Finding - ,URL - ,Details) - SELECT 69 - ,DB_NAME() - ,170 - ,''File Configuration'' - ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' - ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' - FROM #LogInfo - WHERE EXISTS (SELECT name FROM master.sys.databases - WHERE source_database_id is null) OPTION (RECOMPILE); - END - TRUNCATE TABLE #LogInfo;'; - DROP TABLE #LogInfo; - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 80 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 80) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://BrentOzar.com/go/maxsize'', (''The ['' + DB_NAME() + ''] database file '' + name + '' has a max file size set to '' + CAST(CAST(max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') FROM sys.database_files WHERE max_size <> 268435456 AND max_size <> -1 AND type <> 2 AND name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; - END; - - - /* Check if columnstore indexes are in use - for Github issue #615 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ - BEGIN - TRUNCATE TABLE #TemporaryDatabaseResults; - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; IF EXISTS(SELECT * FROM sys.indexes WHERE type IN (5,6)) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; - IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @ColumnStoreIndexesInUse = 1; - END; - - - /* Non-Default Database Scoped Config - Github issue #598 */ - IF EXISTS ( SELECT * FROM sys.all_objects WHERE [name] = 'database_scoped_configurations' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] through [%d].', 0, 1, 194, 197) 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; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) - SELECT def1.CheckID, DB_NAME(), 210, ''Non-Default Database Scoped Config'', dsc.[name], ''https://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 - LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (dsc.value = def.default_value 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);'; - END; - - - - - END; /* IF @CheckUserDatabaseObjects = 1 */ - - IF @CheckProcedureCache = 1 - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Begin checking procedure cache', 0, 1) WITH NOWAIT; - - BEGIN - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 35 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 35) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 35 AS CheckID , - 100 AS Priority , - 'Performance' AS FindingsGroup , - 'Single-Use Plans in Procedure Cache' AS Finding , - 'https://BrentOzar.com/go/single' AS URL , - ( CAST(COUNT(*) AS VARCHAR(10)) - + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details - FROM sys.dm_exec_cached_plans AS cp - WHERE cp.usecounts = 1 - AND cp.objtype = 'Adhoc' - AND EXISTS ( SELECT - 1 - FROM sys.configurations - WHERE - name = 'optimize for ad hoc workloads' - AND value_in_use = 0 ) - HAVING COUNT(*) > 1; - END; - - - /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ - IF @@VERSION LIKE '%Microsoft SQL Server 2005%' - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - END; - IF @ProductVersionMajor >= 10 - BEGIN - IF @CheckProcedureCacheFilter = 'CPU' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_worker_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Reads' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_logical_reads DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'ExecCount' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.execution_count DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - IF @CheckProcedureCacheFilter = 'Duration' - OR @CheckProcedureCacheFilter IS NULL - BEGIN - SET @StringToExecute = 'WITH queries ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - AS (SELECT TOP 20 qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM sys.dm_exec_query_stats qs - ORDER BY qs.total_elapsed_time DESC) - INSERT INTO #dm_exec_query_stats ([sql_handle],[statement_start_offset],[statement_end_offset],[plan_generation_num],[plan_handle],[creation_time],[last_execution_time],[execution_count],[total_worker_time],[last_worker_time],[min_worker_time],[max_worker_time],[total_physical_reads],[last_physical_reads],[min_physical_reads],[max_physical_reads],[total_logical_writes],[last_logical_writes],[min_logical_writes],[max_logical_writes],[total_logical_reads],[last_logical_reads],[min_logical_reads],[max_logical_reads],[total_clr_time],[last_clr_time],[min_clr_time],[max_clr_time],[total_elapsed_time],[last_elapsed_time],[min_elapsed_time],[max_elapsed_time],[query_hash],[query_plan_hash]) - SELECT qs.[sql_handle],qs.[statement_start_offset],qs.[statement_end_offset],qs.[plan_generation_num],qs.[plan_handle],qs.[creation_time],qs.[last_execution_time],qs.[execution_count],qs.[total_worker_time],qs.[last_worker_time],qs.[min_worker_time],qs.[max_worker_time],qs.[total_physical_reads],qs.[last_physical_reads],qs.[min_physical_reads],qs.[max_physical_reads],qs.[total_logical_writes],qs.[last_logical_writes],qs.[min_logical_writes],qs.[max_logical_writes],qs.[total_logical_reads],qs.[last_logical_reads],qs.[min_logical_reads],qs.[max_logical_reads],qs.[total_clr_time],qs.[last_clr_time],qs.[min_clr_time],qs.[max_clr_time],qs.[total_elapsed_time],qs.[last_elapsed_time],qs.[min_elapsed_time],qs.[max_elapsed_time],qs.[query_hash],qs.[query_plan_hash] - FROM queries qs - LEFT OUTER JOIN #dm_exec_query_stats qsCaught ON qs.sql_handle = qsCaught.sql_handle AND qs.plan_handle = qsCaught.plan_handle AND qs.statement_start_offset = qsCaught.statement_start_offset - WHERE qsCaught.sql_handle IS NULL OPTION (RECOMPILE);'; - EXECUTE(@StringToExecute); - END; - - /* Populate the query_plan_filtered field. Only works in 2005SP2+, but we're just doing it in 2008 to be safe. */ - UPDATE #dm_exec_query_stats - SET query_plan_filtered = qp.query_plan - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_text_query_plan(qs.plan_handle, - qs.statement_start_offset, - qs.statement_end_offset) - AS qp; - - END; - - /* Populate the additional query_plan, text, and text_filtered fields */ - UPDATE #dm_exec_query_stats - SET query_plan = qp.query_plan , - [text] = st.[text] , - text_filtered = SUBSTRING(st.text, - ( qs.statement_start_offset - / 2 ) + 1, - ( ( CASE qs.statement_end_offset - WHEN -1 - THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - - qs.statement_start_offset ) - / 2 ) + 1) - FROM #dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) - AS qp; - - /* Dump instances of our own script. We're not trying to tune ourselves. */ - DELETE #dm_exec_query_stats - WHERE text LIKE '%sp_Blitz%' - OR text LIKE '%#BlitzResults%'; - - /* Look for implicit conversions */ - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 63 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 63) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 63 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%CONVERT_IMPLICIT%' - AND COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%PhysicalOp="Index Scan"%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 64 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 64) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details , - QueryPlan , - QueryPlanFiltered - ) - SELECT 64 AS CheckID , - 120 AS Priority , - 'Query Plans' AS FindingsGroup , - 'Implicit Conversion Affecting Cardinality' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , - ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , - qs.query_plan , - qs.query_plan_filtered - FROM #dm_exec_query_stats qs - WHERE COALESCE(qs.query_plan_filtered, - CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 187 ) - - IF SERVERPROPERTY('IsHadrEnabled') = 1 - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 187) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 187 AS [CheckID] , - 230 AS [Priority] , - 'Security' AS [FindingsGroup] , - 'Endpoints Owned by Users' AS [Finding] , - 'https://BrentOzar.com/go/owners' AS [URL] , - ( 'Endpoint ' + ep.[name] + ' is owned by ' + SUSER_NAME(ep.principal_id) + '. If the endpoint owner login is disabled or not available due to Active Directory problems, the high availability will stop working.' - ) AS [Details] - FROM sys.database_mirroring_endpoints ep - LEFT OUTER JOIN sys.dm_server_services s ON SUSER_NAME(ep.principal_id) = s.service_account - WHERE s.service_account IS NULL AND ep.principal_id <> 1; - END; - - /*Check for the last good DBCC CHECKDB date */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 68 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT #DBCCs - (ParentObject, - Object, - Field, - Value) - EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); - UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; - - WITH DB2 - AS ( SELECT DISTINCT - Field , - Value , - DbName - FROM #DBCCs - WHERE Field = 'dbi_dbccLastKnownGood' - ) - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 68 AS CheckID , - DB2.DbName AS DatabaseName , - 1 AS PRIORITY , - 'Reliability' AS FindingsGroup , - 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , - 'https://BrentOzar.com/go/checkdb' AS URL , - 'Last successful CHECKDB: ' - + CASE DB2.Value - WHEN '1900-01-01 00:00:00.000' - THEN ' never.' - ELSE DB2.Value - END AS Details - FROM DB2 - WHERE DB2.DbName <> 'tempdb' - AND DB2.DbName NOT IN ( SELECT DISTINCT - DatabaseName - FROM - #SkipChecks - WHERE CheckID IS NULL OR CheckID = 68) - AND DB2.DbName NOT IN ( SELECT name - FROM sys.databases - WHERE is_read_only = 1) - AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, - -14, - CURRENT_TIMESTAMP); - END; - - - - - /*Verify that the servername is set */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 70 ) - BEGIN - IF @@SERVERNAME IS NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - '@@Servername Not Set' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details; - END; - - IF /* @@SERVERNAME IS set */ - (@@SERVERNAME IS NOT NULL - AND - /* not a named instance */ - CHARINDEX('\',CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 - AND - /* not clustered, when computername may be different than the servername */ - SERVERPROPERTY('IsClustered') = 0 - AND - /* @@SERVERNAME is different than the computer name */ - @@SERVERNAME <> CAST(ISNULL(SERVERPROPERTY('ComputerNamePhysicalNetBIOS'),@@SERVERNAME) AS NVARCHAR(128)) ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 70) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 70 AS CheckID , - 200 AS Priority , - 'Configuration' AS FindingsGroup , - '@@Servername Not Correct' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details; - END; - - END; - /*Check to see if a failsafe operator has been configured*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 73 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 73) WITH NOWAIT; - - DECLARE @AlertInfo TABLE - ( - FailSafeOperator NVARCHAR(255) , - NotificationMethod INT , - ForwardingServer NVARCHAR(255) , - ForwardingSeverity INT , - PagerToTemplate NVARCHAR(255) , - PagerCCTemplate NVARCHAR(255) , - PagerSubjectTemplate NVARCHAR(255) , - PagerSendSubjectOnly NVARCHAR(255) , - ForwardAlways INT - ); - INSERT INTO @AlertInfo - EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 73 AS CheckID , - 200 AS Priority , - 'Monitoring' AS FindingsGroup , - 'No failsafe operator configured' AS Finding , - 'https://BrentOzar.com/go/failsafe' AS URL , - ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details - FROM @AlertInfo - WHERE FailSafeOperator IS NULL; - END; - -/*Identify globally enabled trace flags*/ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 74 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - - INSERT INTO #TraceStatus - EXEC ( ' DBCC TRACESTATUS(-1) WITH NO_INFOMSGS' - ); - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 74 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'TraceFlag On' AS Finding , - CASE WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN 'https://support.microsoft.com/en-us/kb/3210239' - ELSE'https://www.BrentOzar.com/go/traceflags/' END AS URL , - 'Trace flag ' + - CASE WHEN [T].[TraceFlag] = '2330' THEN ' 2330 enabled globally. Using this trace Flag disables missing index requests' - WHEN [T].[TraceFlag] = '1211' THEN ' 1211 enabled globally. Using this Trace Flag disables lock escalation when you least expect it. No Bueno!' - WHEN [T].[TraceFlag] = '1224' THEN ' 1224 enabled globally. Using this Trace Flag disables lock escalation based on the number of locks being taken. You shouldn''t have done that, Dave.' - WHEN [T].[TraceFlag] = '652' THEN ' 652 enabled globally. Using this Trace Flag disables pre-fetching during index scans. If you hate slow queries, you should turn that off.' - WHEN [T].[TraceFlag] = '661' THEN ' 661 enabled globally. Using this Trace Flag disables ghost record removal. Who you gonna call? No one, turn that thing off.' - WHEN [T].[TraceFlag] = '1806' THEN ' 1806 enabled globally. Using this Trace Flag disables instant file initialization. I question your sanity.' - WHEN [T].[TraceFlag] = '3505' THEN ' 3505 enabled globally. Using this Trace Flag disables Checkpoints. Probably not the wisest idea.' - WHEN [T].[TraceFlag] = '8649' THEN ' 8649 enabled globally. Using this Trace Flag drops cost thresholf for parallelism down to 0. I hope this is a dev server.' - WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN ' 834 is enabled globally. Using this Trace Flag with Columnstore Indexes is not a great idea.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN ' 8017 is enabled globally, which is the default for express edition.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN ' 8017 is enabled globally. Using this Trace Flag disables creation schedulers for all logical processors. Not good.' - ELSE [T].[TraceFlag] + ' is enabled globally.' END - AS Details - FROM #TraceStatus T; - END; - - /*Check for transaction log file larger than data file */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 75 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 75) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 75 AS CheckID , - DB_NAME(a.database_id) , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Transaction Log Larger than Data File' AS Finding , - 'https://BrentOzar.com/go/biglog' AS URL , - 'The database [' + DB_NAME(a.database_id) - + '] has a ' + CAST((CAST(a.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(20)) + ' GB transaction log file, larger than the total data file sizes. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details - FROM sys.master_files a - WHERE a.type = 1 - AND DB_NAME(a.database_id) NOT IN ( - SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID = 75 OR CheckID IS NULL) - AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ - AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) - FROM sys.master_files b - WHERE a.database_id = b.database_id - AND b.type = 0 - ) - AND a.database_id IN ( - SELECT database_id - FROM sys.databases - WHERE source_database_id IS NULL ); - END; - - /*Check for collation conflicts between user databases and tempdb */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 76 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 76) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 76 AS CheckID , - name AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Collation is ' + collation_name AS Finding , - 'https://BrentOzar.com/go/collate' AS URL , - 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details - FROM sys.databases - WHERE name NOT IN ( 'master', 'model', 'msdb') - AND name NOT LIKE 'ReportServer%' - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 76) - AND collation_name <> ( SELECT - collation_name - FROM - sys.databases - WHERE - name = 'tempdb' - ); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 77 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 77) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 77 AS CheckID , - dSnap.[name] AS DatabaseName , - 50 AS Priority , - 'Reliability' AS FindingsGroup , - 'Database Snapshot Online' AS Finding , - 'https://BrentOzar.com/go/snapshot' AS URL , - 'Database [' + dSnap.[name] - + '] is a snapshot of [' - + dOriginal.[name] - + ']. Make sure you have enough drive space to maintain the snapshot as the original database grows.' AS Details - FROM sys.databases dSnap - INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id - AND dSnap.name NOT IN ( - SELECT DISTINCT DatabaseName - FROM #SkipChecks - WHERE CheckID = 77 OR CheckID IS NULL); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 79 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 79) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 79 AS CheckID , - -- sp_Blitz Issue #776 - -- Job has history and was executed in the last 30 days OR Job is enabled AND Job Schedule is enabled - CASE WHEN (cast(datediff(dd, substring(cast(sjh.run_date as nvarchar(10)), 1, 4) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 5, 2) + '-' + substring(cast(sjh.run_date as nvarchar(10)), 7, 2), GETDATE()) AS INT) < 30) OR (j.[enabled] = 1 AND ssc.[enabled] = 1 )THEN - 100 - ELSE -- no job history (implicit) AND job not run in the past 30 days AND (Job disabled OR Job Schedule disabled) - 200 - END AS Priority, - 'Performance' AS FindingsGroup , - 'Shrink Database Job' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , - 'In the [' + j.[name] + '] job, step [' - + step.[step_name] - + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' - + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS Details - FROM msdb.dbo.sysjobs j - INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc - ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc - ON sjsc.schedule_id = ssc.schedule_id - AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id - AND step.step_id = sjh.step_id - AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date - AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time - WHERE step.command LIKE N'%SHRINKDATABASE%' - OR step.command LIKE N'%SHRINKFILE%'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 81 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 81) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 81 AS CheckID , - 200 AS Priority , - 'Non-Active Server Config' AS FindingsGroup , - cr.name AS Finding , - 'https://www.BrentOzar.com/blitz/sp_configure/' AS URL , - ( 'This sp_configure option isn''t running under its set value. Its set value is ' - + CAST(cr.[value] AS VARCHAR(100)) - + ' and its running value is ' - + CAST(cr.value_in_use AS VARCHAR(100)) - + '. When someone does a RECONFIGURE or restarts the instance, this setting will start taking effect.' ) AS Details - FROM sys.configurations cr - WHERE cr.value <> cr.value_in_use - AND NOT (cr.name = 'min server memory (MB)' AND cr.value IN (0,16) AND cr.value_in_use IN (0,16)); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 123 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 123) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 1 123 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Agent Jobs Starting Simultaneously' AS Finding , - 'https://BrentOzar.com/go/busyagent/' AS URL , - ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details - FROM msdb.dbo.sysjobactivity - WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) - GROUP BY start_execution_date HAVING COUNT(*) > 1; - END; - - - IF @CheckServerInfo = 1 - BEGIN - -/*This checks Windows version. It would be better if Microsoft gave everything a separate build number, but whatever.*/ -IF @ProductVersionMajor >= 10 - AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 172 ) - BEGIN - -- sys.dm_os_host_info includes both Windows and Linux info - IF EXISTS (SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Operating System Version' AS [Finding] , - ( CASE WHEN @IsWindowsOperatingSystem = 1 - THEN 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' - ELSE 'https://en.wikipedia.org/wiki/List_of_Linux_distributions' - END - ) AS [URL] , - ( CASE - WHEN [ohi].[host_platform] = 'Linux' THEN 'You''re running the ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ' distribution of ' + CAST([ohi].[host_platform] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' AND [ohi].[host_release] < '6' THEN 'You''re running a really old version: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] >= '6' AND [ohi].[host_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.2' THEN 'You''re running a rather modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - ELSE 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - END - ) AS [Details] - FROM [sys].[dm_os_host_info] [ohi]; - END; - ELSE - BEGIN - -- Otherwise, stick with Windows-only detection - - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_windows_info' ) - - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - - SELECT - 172 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Windows Version' AS [Finding] , - 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' AS [URL] , - ( CASE - WHEN [owi].[windows_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running a really old version: Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.2' THEN 'You''re running a rather modern version of Windows: Server 2012 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: Server 2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - ELSE 'I have no idea which version of Windows you''re on. Sorry.' - END - ) AS [Details] - FROM [sys].[dm_os_windows_info] [owi]; - - END; - END; - END; - -/* -This check hits the dm_os_process_memory system view -to see if locked_page_allocations_kb is > 0, -which could indicate that locked pages in memory is enabled. -*/ -IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - INSERT INTO [#BlitzResults] - ( [CheckID] , - [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - [Details] ) - SELECT - 166 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Locked Pages In Memory Enabled' AS [Finding] , - 'https://BrentOzar.com/go/lpim' AS [URL] , - ( 'You currently have ' - + CASE WHEN [dopm].[locked_page_allocations_kb] / 1024. / 1024. > 0 - THEN CAST([dopm].[locked_page_allocations_kb] / 1024. / 1024. AS VARCHAR(100)) - + ' GB' - ELSE CAST([dopm].[locked_page_allocations_kb] / 1024. AS VARCHAR(100)) - + ' MB' - END + ' of pages locked in memory.' ) AS [Details] - FROM - [sys].[dm_os_process_memory] AS [dopm] - WHERE - [dopm].[locked_page_allocations_kb] > 0; - END; - - /* Server Info - Locked Pages In Memory Enabled - Check 166 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 166 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'sql_memory_model' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 166 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Memory Model Unconventional'' AS Finding , - ''https://BrentOzar.com/go/lpim'' AS URL , - ''Memory Model: '' + CAST(sql_memory_model_desc AS NVARCHAR(100)) - FROM sys.dm_os_sys_info WHERE sql_memory_model <> 1 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.'; - - EXECUTE(@StringToExecute); - END; - - - - /* - Starting with SQL Server 2014 SP2, Instant File Initialization - is logged in the SQL Server Error Log. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND (@ProductVersionMajor >= 13) OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - INSERT INTO #ErrorLog - EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; - - IF @@ROWCOUNT > 0 - INSERT INTO #BlitzResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 193 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Instant File Initialization Enabled' AS [Finding] , - 'https://BrentOzar.com/go/instant' AS [URL] , - 'The service account has the Perform Volume Maintenance Tasks permission.'; - END; - - /* Server Info - Instant File Initialization Not Enabled - Check 192 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 192 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_server_services' - AND c.name = 'instant_file_initialization_enabled' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 192) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 192 AS CheckID , - 50 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Instant File Initialization Not Enabled'' AS Finding , - ''https://BrentOzar.com/go/instant'' AS URL , - ''Consider enabling IFI for faster restores and data file growths.'' - FROM sys.dm_server_services WHERE instant_file_initialization_enabled <> ''Y'' AND filename LIKE ''%sqlservr.exe%'' 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.'; - - EXECUTE(@StringToExecute); - END; - - - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 130 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 130) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 130 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Name' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , - @@SERVERNAME AS Details - WHERE @@SERVERNAME IS NOT NULL; - END; - - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 83 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects - WHERE name = 'dm_server_services' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 83) WITH NOWAIT; - - -- DATETIMEOFFSET and DATETIME have different minimum values, so there's - -- a small workaround here to force 1753-01-01 if the minimum is detected - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 83 AS CheckID , - 250 AS Priority , - ''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 + ''.'' - FROM sys.dm_server_services 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - /* Check 84 - SQL Server 2012 */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 84 ) - BEGIN - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_kb' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_kb / 1024.0 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - - /* Check 84 - SQL Server 2008 */ - IF EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'physical_memory_in_bytes' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 84) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 84 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware'' AS Finding , - '''' AS URL , - ''Logical processors: '' + CAST(cpu_count AS VARCHAR(50)) + ''. Physical memory: '' + CAST( CAST(ROUND((physical_memory_in_bytes / 1024.0 / 1024 / 1024), 1) AS INT) AS VARCHAR(50)) + ''GB.'' - FROM sys.dm_os_sys_info 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.'; - - EXECUTE(@StringToExecute); - END; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 85 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 85) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 85 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Service' AS Finding , - '' AS URL , - N'Version: ' - + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) - + N'. Patch Level: ' - + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) - + N'. Edition: ' - + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + N'. AlwaysOn Enabled: ' - + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), - 0) AS VARCHAR(100)) - + N'. AlwaysOn Mgr Status: ' - + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), - 0) AS VARCHAR(100)); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 88 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 88) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 88 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'SQL Server Last Restart' AS Finding , - '' AS URL , - CAST(create_date AS VARCHAR(100)) - FROM sys.databases - WHERE database_id = 2; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 91 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 91) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 91 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Server Last Restart' AS Finding , - '' AS URL , - CAST(DATEADD(SECOND, (ms_ticks/1000)*(-1), GETDATE()) AS nvarchar(25)) - FROM sys.dm_os_sys_info; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 92 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 92) WITH NOWAIT; - - INSERT INTO #driveInfo - ( drive, SIZE ) - EXEC master..xp_fixeddrives; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 92 AS CheckID , - 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Drive ' + i.drive + ' Space' AS Finding , - '' AS URL , - CAST(i.SIZE AS VARCHAR(30)) - + 'MB free on ' + i.drive - + ' drive' AS Details - FROM #driveInfo AS i; - DROP TABLE #driveInfo; - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 103 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_sys_info' - AND c.name = 'virtual_machine_type_desc' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 103) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 103 AS CheckID, - 250 AS Priority, - ''Server Info'' AS FindingsGroup, - ''Virtual Server'' AS Finding, - ''https://BrentOzar.com/go/virtual'' AS URL, - ''Type: ('' + virtual_machine_type_desc + '')'' AS Details - FROM sys.dm_os_sys_info - WHERE virtual_machine_type <> 0 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.'; - - EXECUTE(@StringToExecute); - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 114 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - WHERE o.name = 'dm_os_memory_nodes' ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_os_nodes' - AND c.name = 'processor_group' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 114) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 114 AS CheckID , - 250 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Hardware - NUMA Config'' AS Finding , - '''' AS URL , - ''Node: '' + CAST(n.node_id AS NVARCHAR(10)) + '' State: '' + node_state_desc - + '' Online schedulers: '' + CAST(n.online_scheduler_count AS NVARCHAR(10)) + '' Offline schedulers: '' + CAST(oac.offline_schedulers AS VARCHAR(100)) + '' Processor Group: '' + CAST(n.processor_group AS NVARCHAR(10)) - + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) - FROM sys.dm_os_nodes n - INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - OUTER APPLY (SELECT - COUNT(*) AS [offline_schedulers] - FROM sys.dm_os_schedulers dos - WHERE n.node_id = dos.parent_node_id - AND dos.status = ''VISIBLE OFFLINE'' - ) oac - WHERE n.node_state_desc NOT LIKE ''%DAC%'' - ORDER BY n.node_id 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.'; - - EXECUTE(@StringToExecute); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 106 ) - AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 - AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 106) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 106 AS CheckID - ,250 AS Priority - ,'Server Info' AS FindingsGroup - ,'Default Trace Contents' AS Finding - ,'https://BrentOzar.com/go/trace' AS URL - ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as VARCHAR(30))+' hours of data' - +' between '+cast(Min(StartTime) as VARCHAR(30))+' and '+cast(GETDATE()as VARCHAR(30)) - +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) - ) as Details - FROM ::fn_trace_gettable( @base_tracefilename, default ) - WHERE EventClass BETWEEN 65500 and 65600; - END; /* CheckID 106 */ - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 152 ) - BEGIN - IF EXISTS (SELECT * FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 - AND i.wait_type IS NULL) - BEGIN - /* Check for waits that have had more than 10% of the server's wait time */ - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 152) WITH NOWAIT; - - WITH os(wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms) - AS - (SELECT ws.wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms - FROM sys.dm_os_wait_stats ws - LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE i.wait_type IS NULL - AND wait_time_ms > .1 * @CpuMsSinceWaitsCleared - AND waiting_tasks_count > 0) - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT TOP 9 - 152 AS CheckID - ,240 AS Priority - ,'Wait Stats' AS FindingsGroup - , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding - ,'https://BrentOzar.com/go/waits' AS URL - , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' hours of waits, ' + - CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + - /* CAST(CAST( - 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER () ) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% of waits, ' + */ - CAST(CAST( - 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.wait_time_ms) OVER ()) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + - CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + - CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 - THEN - CAST( - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) - AS NUMERIC(18,1)) - ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' - FROM os - ORDER BY SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) DESC; - END; /* IF EXISTS (SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 0 AND waiting_tasks_count > 0) */ - - /* If no waits were found, add a note about that */ - IF NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (107, 108, 109, 121, 152, 162)) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 153) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://BrentOzar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); - END; - END; /* CheckID 152 */ - - END; /* IF @CheckServerInfo = 1 */ - END; /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ - - - /* Delete priorites they wanted to skip. */ - IF @IgnorePrioritiesAbove IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] > @IgnorePrioritiesAbove AND CheckID <> -1; - - IF @IgnorePrioritiesBelow IS NOT NULL - DELETE #BlitzResults - WHERE [Priority] < @IgnorePrioritiesBelow AND CheckID <> -1; - - /* Delete checks they wanted to skip. */ - IF @SkipChecksTable IS NOT NULL - BEGIN - DELETE FROM #BlitzResults - WHERE DatabaseName IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE FROM #BlitzResults - WHERE CheckID IN ( SELECT CheckID - FROM #SkipChecks - WHERE DatabaseName IS NULL - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName'))); - DELETE r FROM #BlitzResults r - INNER JOIN #SkipChecks c ON r.DatabaseName = c.DatabaseName and r.CheckID = c.CheckID - AND (ServerName IS NULL OR ServerName = SERVERPROPERTY('ServerName')); - END; - - /* Add summary mode */ - IF @SummaryMode > 0 - BEGIN - UPDATE #BlitzResults - SET Finding = br.Finding + ' (' + CAST(brTotals.recs AS NVARCHAR(20)) + ')' - FROM #BlitzResults br - INNER JOIN (SELECT FindingsGroup, Finding, Priority, COUNT(*) AS recs FROM #BlitzResults GROUP BY FindingsGroup, Finding, Priority) brTotals ON br.FindingsGroup = brTotals.FindingsGroup AND br.Finding = brTotals.Finding AND br.Priority = brTotals.Priority - WHERE brTotals.recs > 1; - - DELETE br - FROM #BlitzResults br - WHERE EXISTS (SELECT * FROM #BlitzResults brLower WHERE br.FindingsGroup = brLower.FindingsGroup AND br.Finding = brLower.Finding AND br.Priority = brLower.Priority AND br.ID > brLower.ID); - - END; - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org' , - 'We hope you found this tool useful.' - ); - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - VALUES ( -1 , - 0 , - 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - 'SQL Server First Responder Kit' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - - ); - - INSERT INTO #BlitzResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - SELECT 156 , - 254 , - 'Rundate' , - GETDATE() , - 'http://FirstResponderKit.org/' , - 'Captain''s log: stardate something and something...'; - - IF @EmailRecipients IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Sending an email.', 0, 1) WITH NOWAIT; - - /* Database mail won't work off a local temp table. I'm not happy about this hacky workaround either. */ - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - SELECT * INTO ##BlitzResults FROM #BlitzResults; - SET @query_result_separator = char(9); - SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup, Finding, Details; SET NOCOUNT OFF;'; - SET @EmailSubject = 'sp_Blitz Results for ' + @@SERVERNAME; - SET @EmailBody = 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. http://FirstResponderKit.org'; - IF @EmailProfile IS NULL - EXEC msdb.dbo.sp_send_dbmail - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - ELSE - EXEC msdb.dbo.sp_send_dbmail - @profile_name = @EmailProfile, - @recipients = @EmailRecipients, - @subject = @EmailSubject, - @body = @EmailBody, - @query_attachment_filename = 'sp_Blitz-Results.csv', - @attach_query_result_as_file = 1, - @query_result_header = 1, - @query_result_width = 32767, - @append_query_error = 1, - @query_result_no_padding = 1, - @query_result_separator = @query_result_separator, - @query = @StringToExecute; - IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; - END; - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT; - DECLARE @ValidOutputLocation BIT; - DECLARE @LinkedServerDBCheck NVARCHAR(2000); - DECLARE @ValidLinkedServerDB INT; - DECLARE @tmpdbchk table (cnt int); - IF @OutputServerName IS NOT NULL - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; - - IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) - BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); - IF (@ValidLinkedServerDB > 0) - BEGIN - SET @ValidOutputServer = 1; - SET @ValidOutputLocation = 1; - END; - ELSE - RAISERROR('The specified database was not found on the output server', 16, 0); - END; - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0); - END; - END; - ELSE - BEGIN - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1; - END; - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0); - END; - ELSE - BEGIN - SET @ValidOutputLocation = 0; - END; - END; - - /* @OutputTableName lets us export the results to a permanent table */ - IF @ValidOutputLocation = 1 - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));'; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); - SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); - EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END; - ELSE - BEGIN - EXEC(@StringToExecute); - END; - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputServerName + '.' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputServerName + '.' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - IF @ValidOutputServer = 1 - BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END; - ELSE - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' - + 'CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - Priority TINYINT , - FindingsGroup VARCHAR(50) , - Finding VARCHAR(200) , - DatabaseName NVARCHAR(128), - URL VARCHAR(200) , - Details NVARCHAR(4000) , - QueryPlan [XML] NULL , - QueryPlanFiltered [NVARCHAR](MAX) NULL, - CheckID INT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - - EXEC(@StringToExecute); - END; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); - END; - - - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; - - IF @OutputType = 'COUNT' - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzResults; - END; - ELSE - IF @OutputType IN ( 'CSV', 'RSV' ) - BEGIN - - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputXMLasNVARCHAR = 1 AND @OutputType <> 'NONE' - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan, - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - ELSE IF @OutputType = 'MARKDOWN' - BEGIN - WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * - FROM #BlitzResults - WHERE Priority > 0 AND Priority < 255 AND FindingsGroup IS NOT NULL AND Finding IS NOT NULL - AND FindingsGroup <> 'Security' /* Specifically excluding security checks for public exports */) - SELECT - CASE - WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf - ELSE N'' - END - + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END - ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END - END + @crlf - FROM Results r - LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 - LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 - ORDER BY r.rownum FOR XML PATH(N''); - END; - ELSE IF @OutputType <> 'NONE' - BEGIN - /* --TOURSTOP05-- */ - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [DatabaseName] , - [URL] , - [Details] , - [QueryPlan] , - [QueryPlanFiltered] , - CheckID - FROM #BlitzResults - ORDER BY Priority , - FindingsGroup , - Finding , - DatabaseName , - Details; - END; - - DROP TABLE #BlitzResults; - - IF @OutputProcedureCache = 1 - AND @CheckProcedureCache = 1 - SELECT TOP 20 - total_worker_time / execution_count AS AvgCPU , - total_worker_time AS TotalCPU , - CAST(ROUND(100.00 * total_worker_time - / ( SELECT SUM(total_worker_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentCPU , - total_elapsed_time / execution_count AS AvgDuration , - total_elapsed_time AS TotalDuration , - CAST(ROUND(100.00 * total_elapsed_time - / ( SELECT SUM(total_elapsed_time) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - CAST(ROUND(100.00 * total_logical_reads - / ( SELECT SUM(total_logical_reads) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentReads , - execution_count , - CAST(ROUND(100.00 * execution_count - / ( SELECT SUM(execution_count) - FROM sys.dm_exec_query_stats - ), 2) AS MONEY) AS PercentExecutions , - CASE WHEN DATEDIFF(mi, creation_time, - qs.last_execution_time) = 0 THEN 0 - ELSE CAST(( 1.00 * execution_count / DATEDIFF(mi, - creation_time, - qs.last_execution_time) ) AS MONEY) - END AS executions_per_minute , - qs.creation_time AS plan_creation_time , - qs.last_execution_time , - text , - text_filtered , - query_plan , - query_plan_filtered , - sql_handle , - query_hash , - plan_handle , - query_plan_hash - FROM #dm_exec_query_stats qs - ORDER BY CASE UPPER(@CheckProcedureCacheFilter) - WHEN 'CPU' THEN total_worker_time - WHEN 'READS' THEN total_logical_reads - WHEN 'EXECCOUNT' THEN execution_count - WHEN 'DURATION' THEN total_elapsed_time - ELSE total_worker_time - END DESC; - - END; /* ELSE -- IF @OutputType = 'SCHEMA' */ - - SET NOCOUNT OFF; -GO - -/* ---Sample execution call with the most common parameters: -EXEC [dbo].[sp_Blitz] - @CheckUserDatabaseObjects = 1 , - @CheckProcedureCache = 0 , - @OutputType = 'TABLE' , - @OutputProcedureCache = 0 , - @CheckProcedureCacheFilter = NULL, - @CheckServerInfo = 1 -*/ -IF OBJECT_ID('dbo.sp_BlitzBackups') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzBackups AS RETURN 0;'); -GO -ALTER PROCEDURE [dbo].[sp_BlitzBackups] - @Help TINYINT = 0 , - @HoursBack INT = 168, - @MSDBName NVARCHAR(256) = 'msdb', - @AGName NVARCHAR(256) = NULL, - @RestoreSpeedFullMBps INT = NULL, - @RestoreSpeedDiffMBps INT = NULL, - @RestoreSpeedLogMBps INT = NULL, - @Debug TINYINT = 0, - @PushBackupHistoryToListener BIT = 0, - @WriteBackupsToListenerName NVARCHAR(256) = NULL, - @WriteBackupsToDatabaseName NVARCHAR(256) = NULL, - @WriteBackupsLastHours INT = 168, - @VersionDate DATE = NULL OUTPUT -WITH RECOMPILE -AS - BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - - IF @Help = 1 PRINT ' - /* - sp_BlitzBackups from http://FirstResponderKit.org - - This script checks your backups to see how much data you might lose when - this server fails, and how long it might take to recover. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - Parameter explanations: - - @HoursBack INT = 168 How many hours of history to examine, back from now. - You can check just the last 24 hours of backups, for example. - @MSDBName NVARCHAR(255) You can restore MSDB from different servers and check them - centrally. Also useful if you create a DBA utility database - and merge data from several servers in an AG into one DB. - @RestoreSpeedFullMBps INT By default, we use the backup speed from MSDB to guesstimate - how fast your restores will go. If you have done performance - tuning and testing of your backups (or if they horribly go even - slower in your DR environment, and you want to account for - that), then you can pass in different numbers here. - @RestoreSpeedDiffMBps INT See above. - @RestoreSpeedLogMBps INT See above. - - For more documentation: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - - - - */'; -ELSE -BEGIN -DECLARE @StringToExecute NVARCHAR(MAX) = N'', - @InnerStringToExecute NVARCHAR(MAX) = N'', - @ProductVersion NVARCHAR(128), - @ProductVersionMajor DECIMAL(10, 2), - @ProductVersionMinor DECIMAL(10, 2), - @StartTime DATETIME2, @ResultText NVARCHAR(MAX), - @crlf NVARCHAR(2), - @MoreInfoHeader NVARCHAR(100), - @MoreInfoFooter NVARCHAR(100); - -IF @HoursBack > 0 - SET @HoursBack = @HoursBack * -1; - -IF @WriteBackupsLastHours > 0 - SET @WriteBackupsLastHours = @WriteBackupsLastHours * -1; - -SELECT @crlf = NCHAR(13) + NCHAR(10), - @StartTime = DATEADD(hh, @HoursBack, GETDATE()), - @MoreInfoHeader = N''; - -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); - -CREATE TABLE #Backups -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - RPOWorstCaseMinutes DECIMAL(18, 1), - RTOWorstCaseMinutes DECIMAL(18, 1), - RPOWorstCaseBackupSetID INT, - RPOWorstCaseBackupSetFinishTime DATETIME, - RPOWorstCaseBackupSetIDPrior INT, - RPOWorstCaseBackupSetPriorFinishTime DATETIME, - RPOWorstCaseMoreInfoQuery XML, - RTOWorstCaseBackupFileSizeMB DECIMAL(18, 2), - RTOWorstCaseMoreInfoQuery XML, - FullMBpsAvg DECIMAL(18, 2), - FullMBpsMin DECIMAL(18, 2), - FullMBpsMax DECIMAL(18, 2), - FullSizeMBAvg DECIMAL(18, 2), - FullSizeMBMin DECIMAL(18, 2), - FullSizeMBMax DECIMAL(18, 2), - FullCompressedSizeMBAvg DECIMAL(18, 2), - FullCompressedSizeMBMin DECIMAL(18, 2), - FullCompressedSizeMBMax DECIMAL(18, 2), - DiffMBpsAvg DECIMAL(18, 2), - DiffMBpsMin DECIMAL(18, 2), - DiffMBpsMax DECIMAL(18, 2), - DiffSizeMBAvg DECIMAL(18, 2), - DiffSizeMBMin DECIMAL(18, 2), - DiffSizeMBMax DECIMAL(18, 2), - DiffCompressedSizeMBAvg DECIMAL(18, 2), - DiffCompressedSizeMBMin DECIMAL(18, 2), - DiffCompressedSizeMBMax DECIMAL(18, 2), - LogMBpsAvg DECIMAL(18, 2), - LogMBpsMin DECIMAL(18, 2), - LogMBpsMax DECIMAL(18, 2), - LogSizeMBAvg DECIMAL(18, 2), - LogSizeMBMin DECIMAL(18, 2), - LogSizeMBMax DECIMAL(18, 2), - LogCompressedSizeMBAvg DECIMAL(18, 2), - LogCompressedSizeMBMin DECIMAL(18, 2), - LogCompressedSizeMBMax DECIMAL(18, 2) -); - -CREATE TABLE #RTORecoveryPoints -( - id INT IDENTITY(1, 1), - database_name NVARCHAR(128), - database_guid UNIQUEIDENTIFIER, - rto_worst_case_size_mb AS - ( COALESCE(log_file_size_mb, 0) + COALESCE(diff_file_size_mb, 0) + COALESCE(full_file_size_mb, 0)), - rto_worst_case_time_seconds AS - ( COALESCE(log_time_seconds, 0) + COALESCE(diff_time_seconds, 0) + COALESCE(full_time_seconds, 0)), - full_backup_set_id INT, - full_last_lsn NUMERIC(25, 0), - full_backup_set_uuid UNIQUEIDENTIFIER, - full_time_seconds BIGINT, - full_file_size_mb DECIMAL(18, 2), - diff_backup_set_id INT, - diff_last_lsn NUMERIC(25, 0), - diff_time_seconds BIGINT, - diff_file_size_mb DECIMAL(18, 2), - log_backup_set_id INT, - log_last_lsn NUMERIC(25, 0), - log_time_seconds BIGINT, - log_file_size_mb DECIMAL(18, 2), - log_backups INT -); - -CREATE TABLE #Recoverability - ( - Id INT IDENTITY , - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - LastBackupRecoveryModel NVARCHAR(60), - FirstFullBackupSizeMB DECIMAL (18,2), - FirstFullBackupDate DATETIME, - LastFullBackupSizeMB DECIMAL (18,2), - LastFullBackupDate DATETIME, - AvgFullBackupThroughputMB DECIMAL (18,2), - AvgFullBackupDurationSeconds INT, - AvgDiffBackupThroughputMB DECIMAL (18,2), - AvgDiffBackupDurationSeconds INT, - AvgLogBackupThroughputMB DECIMAL (18,2), - AvgLogBackupDurationSeconds INT, - AvgFullSizeMB DECIMAL (18,2), - AvgDiffSizeMB DECIMAL (18,2), - AvgLogSizeMB DECIMAL (18,2), - IsBigDiff AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgDiffSizeMB * 100.) / AvgFullSizeMB >= 40.)) THEN 1 ELSE 0 END, - IsBigLog AS CASE WHEN (AvgFullSizeMB > 10240. AND ((AvgLogSizeMB * 100.) / AvgFullSizeMB >= 20.)) THEN 1 ELSE 0 END - ); - -CREATE TABLE #Trending -( - DatabaseName NVARCHAR(128), - DatabaseGUID UNIQUEIDENTIFIER, - [0] DECIMAL(18, 2), - [-1] DECIMAL(18, 2), - [-2] DECIMAL(18, 2), - [-3] DECIMAL(18, 2), - [-4] DECIMAL(18, 2), - [-5] DECIMAL(18, 2), - [-6] DECIMAL(18, 2), - [-7] DECIMAL(18, 2), - [-8] DECIMAL(18, 2), - [-9] DECIMAL(18, 2), - [-10] DECIMAL(18, 2), - [-11] DECIMAL(18, 2), - [-12] DECIMAL(18, 2) -); - - -CREATE TABLE #Warnings -( - Id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckId INT, - Priority INT, - DatabaseName VARCHAR(128), - Finding VARCHAR(256), - Warning VARCHAR(8000) -); - -IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = @MSDBName) - BEGIN - RAISERROR('@MSDBName was specified, but the database does not exist.', 0, 1) WITH NOWAIT; - RETURN; - END - -IF @PushBackupHistoryToListener = 1 -GOTO PushBackupHistoryToListener - - - RAISERROR('Inserting to #Backups', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'WITH Backups AS (SELECT bs.database_name, bs.database_guid, bs.type AS backup_type ' + @crlf - + ' , MBpsAvg = CAST(AVG(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMin = CAST(MIN(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , MBpsMax = CAST(MAX(( bs.backup_size / ( CASE WHEN DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) = 0 THEN 1 ELSE DATEDIFF(ss, bs.backup_start_date, bs.backup_finish_date) END ) / 1048576 )) AS INT) ' + @crlf - + ' , SizeMBAvg = AVG(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMin = MIN(backup_size / 1048576.0) ' + @crlf - + ' , SizeMBMax = MAX(backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBAvg = AVG(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMin = MIN(compressed_backup_size / 1048576.0) ' + @crlf - + ' , CompressedSizeMBMax = MAX(compressed_backup_size / 1048576.0) ' + @crlf; - - - SET @StringToExecute += N' FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bs ' + @crlf - + N' WHERE bs.backup_finish_date >= @StartTime AND bs.is_damaged = 0 ' + @crlf - + N' GROUP BY bs.database_name, bs.database_guid, bs.type)' + @crlf; - - SET @StringToExecute += + N'INSERT INTO #Backups(database_name, database_guid, ' + @crlf - + N' FullMBpsAvg, FullMBpsMin, FullMBpsMax, FullSizeMBAvg, FullSizeMBMin, FullSizeMBMax, FullCompressedSizeMBAvg, FullCompressedSizeMBMin, FullCompressedSizeMBMax, ' + @crlf - + N' DiffMBpsAvg, DiffMBpsMin, DiffMBpsMax, DiffSizeMBAvg, DiffSizeMBMin, DiffSizeMBMax, DiffCompressedSizeMBAvg, DiffCompressedSizeMBMin, DiffCompressedSizeMBMax, ' + @crlf - + N' LogMBpsAvg, LogMBpsMin, LogMBpsMax, LogSizeMBAvg, LogSizeMBMin, LogSizeMBMax, LogCompressedSizeMBAvg, LogCompressedSizeMBMin, LogCompressedSizeMBMax ) ' + @crlf - + N'SELECT bF.database_name, bF.database_guid ' + @crlf - + N' , bF.MBpsAvg AS FullMBpsAvg ' + @crlf - + N' , bF.MBpsMin AS FullMBpsMin ' + @crlf - + N' , bF.MBpsMax AS FullMBpsMax ' + @crlf - + N' , bF.SizeMBAvg AS FullSizeMBAvg ' + @crlf - + N' , bF.SizeMBMin AS FullSizeMBMin ' + @crlf - + N' , bF.SizeMBMax AS FullSizeMBMax ' + @crlf - + N' , bF.CompressedSizeMBAvg AS FullCompressedSizeMBAvg ' + @crlf - + N' , bF.CompressedSizeMBMin AS FullCompressedSizeMBMin ' + @crlf - + N' , bF.CompressedSizeMBMax AS FullCompressedSizeMBMax ' + @crlf - + N' , bD.MBpsAvg AS DiffMBpsAvg ' + @crlf - + N' , bD.MBpsMin AS DiffMBpsMin ' + @crlf - + N' , bD.MBpsMax AS DiffMBpsMax ' + @crlf - + N' , bD.SizeMBAvg AS DiffSizeMBAvg ' + @crlf - + N' , bD.SizeMBMin AS DiffSizeMBMin ' + @crlf - + N' , bD.SizeMBMax AS DiffSizeMBMax ' + @crlf - + N' , bD.CompressedSizeMBAvg AS DiffCompressedSizeMBAvg ' + @crlf - + N' , bD.CompressedSizeMBMin AS DiffCompressedSizeMBMin ' + @crlf - + N' , bD.CompressedSizeMBMax AS DiffCompressedSizeMBMax ' + @crlf - + N' , bL.MBpsAvg AS LogMBpsAvg ' + @crlf - + N' , bL.MBpsMin AS LogMBpsMin ' + @crlf - + N' , bL.MBpsMax AS LogMBpsMax ' + @crlf - + N' , bL.SizeMBAvg AS LogSizeMBAvg ' + @crlf - + N' , bL.SizeMBMin AS LogSizeMBMin ' + @crlf - + N' , bL.SizeMBMax AS LogSizeMBMax ' + @crlf - + N' , bL.CompressedSizeMBAvg AS LogCompressedSizeMBAvg ' + @crlf - + N' , bL.CompressedSizeMBMin AS LogCompressedSizeMBMin ' + @crlf - + N' , bL.CompressedSizeMBMax AS LogCompressedSizeMBMax ' + @crlf - + N' FROM Backups bF ' + @crlf - + N' LEFT OUTER JOIN Backups bD ON bF.database_name = bD.database_name AND bF.database_guid = bD.database_guid AND bD.backup_type = ''I''' + @crlf - + N' LEFT OUTER JOIN Backups bL ON bF.database_name = bL.database_name AND bF.database_guid = bL.database_guid AND bL.backup_type = ''L''' + @crlf - + N' WHERE bF.backup_type = ''D''; ' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - RAISERROR('Updating #Backups with worst RPO case', 0, 1) WITH NOWAIT; - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - SELECT bs.database_name, bs.database_guid, bs.backup_set_id, bsPrior.backup_set_id AS backup_set_id_prior, - bs.backup_finish_date, bsPrior.backup_finish_date AS backup_finish_date_prior, - DATEDIFF(ss, bsPrior.backup_finish_date, bs.backup_finish_date) AS backup_gap_seconds - INTO #backup_gaps - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs - CROSS APPLY ( - SELECT TOP 1 bs1.backup_set_id, bs1.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS bs1 - WHERE bs.database_name = bs1.database_name - AND bs.database_guid = bs1.database_guid - AND bs.backup_finish_date > bs1.backup_finish_date - AND bs.backup_set_id > bs1.backup_set_id - ORDER BY bs1.backup_finish_date DESC, bs1.backup_set_id DESC - ) bsPrior - WHERE bs.backup_finish_date > @StartTime - - CREATE CLUSTERED INDEX cx_backup_gaps ON #backup_gaps (database_name, database_guid, backup_set_id, backup_finish_date, backup_gap_seconds); - - WITH max_gaps AS ( - SELECT g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, - g.backup_finish_date, MAX(g.backup_gap_seconds) AS max_backup_gap_seconds - FROM #backup_gaps AS g - GROUP BY g.database_name, g.database_guid, g.backup_set_id, g.backup_set_id_prior, g.backup_finish_date_prior, g.backup_finish_date - ) - UPDATE #Backups - SET RPOWorstCaseMinutes = bg.max_backup_gap_seconds / 60.0 - , RPOWorstCaseBackupSetID = bg.backup_set_id - , RPOWorstCaseBackupSetFinishTime = bg.backup_finish_date - , RPOWorstCaseBackupSetIDPrior = bg.backup_set_id_prior - , RPOWorstCaseBackupSetPriorFinishTime = bg.backup_finish_date_prior - FROM #Backups b - INNER HASH JOIN max_gaps bg ON b.database_name = bg.database_name AND b.database_guid = bg.database_guid - LEFT OUTER HASH JOIN max_gaps bgBigger ON bg.database_name = bgBigger.database_name AND bg.database_guid = bgBigger.database_guid AND bg.max_backup_gap_seconds < bgBigger.max_backup_gap_seconds - WHERE bgBigger.backup_set_id IS NULL; - '; - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - RAISERROR('Updating #Backups with worst RPO case queries', 0, 1) WITH NOWAIT; - - UPDATE #Backups - SET RPOWorstCaseMoreInfoQuery = @MoreInfoHeader + N'SELECT * ' + @crlf - + N' FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset ' + @crlf - + N' WHERE database_name = ''' + database_name + ''' ' + @crlf - + N' AND database_guid = ''' + CAST(database_guid AS NVARCHAR(50)) + ''' ' + @crlf - + N' AND backup_finish_date >= DATEADD(hh, -2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' AND backup_finish_date <= DATEADD(hh, 2, ''' + CAST(CONVERT(DATETIME, RPOWorstCaseBackupSetPriorFinishTime, 102) AS NVARCHAR(100)) + ''') ' + @crlf - + N' ORDER BY backup_finish_date;' - + @MoreInfoFooter; - - -/* RTO */ - -RAISERROR('Gathering RTO information', 0, 1) WITH NOWAIT; - - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, log_last_lsn) - SELECT database_name, database_guid, MAX(last_lsn) AS log_last_lsn - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset bLastLog - WHERE type = ''L'' - AND bLastLog.backup_finish_date >= @StartTime - GROUP BY database_name, database_guid; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/* Find the most recent full backups for those logs */ - -RAISERROR('Updating #RTORecoveryPoints', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET log_backup_set_id = bLasted.backup_set_id - ,full_backup_set_id = bLasted.backup_set_id - ,full_last_lsn = bLasted.last_lsn - ,full_backup_set_uuid = bLasted.backup_set_uuid - FROM #RTORecoveryPoints rp - CROSS APPLY ( - SELECT TOP 1 bLog.backup_set_id AS backup_set_id_log, bLastFull.backup_set_id, bLastFull.last_lsn, bLastFull.backup_set_uuid, bLastFull.database_guid, bLastFull.database_name - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLastFull - ON bLog.database_guid = bLastFull.database_guid - AND bLog.database_name = bLastFull.database_name - AND bLog.first_lsn > bLastFull.last_lsn - AND bLastFull.type = ''D'' - WHERE rp.database_guid = bLog.database_guid - AND rp.database_name = bLog.database_name - ) bLasted - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLaterFulls ON bLasted.database_guid = bLaterFulls.database_guid AND bLasted.database_name = bLaterFulls.database_name - AND bLasted.last_lsn < bLaterFulls.last_lsn - AND bLaterFulls.first_lsn < bLasted.last_lsn - AND bLaterFulls.type = ''D'' - WHERE bLaterFulls.backup_set_id IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Add any full backups in the StartDate range that weren't part of the above log backup chain */ - -RAISERROR('Add any full backups in the StartDate range that weren''t part of the above log backup chain', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - INSERT INTO #RTORecoveryPoints(database_name, database_guid, full_backup_set_id, full_last_lsn, full_backup_set_uuid) - SELECT bFull.database_name, bFull.database_guid, bFull.backup_set_id, bFull.last_lsn, bFull.backup_set_uuid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull - LEFT OUTER JOIN #RTORecoveryPoints rp ON bFull.backup_set_uuid = rp.full_backup_set_uuid - WHERE bFull.type = ''D'' - AND bFull.backup_finish_date IS NOT NULL - AND rp.full_backup_set_uuid IS NULL - AND bFull.backup_finish_date >= @StartTime; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/* Fill out the most recent log for that full, but before the next full */ - -RAISERROR('Fill out the most recent log for that full, but before the next full', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE rp - SET log_last_lsn = (SELECT MAX(last_lsn) FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog WHERE bLog.first_lsn >= rp.full_last_lsn AND bLog.first_lsn <= rpNextFull.full_last_lsn AND bLog.type = ''L'') - FROM #RTORecoveryPoints rp - INNER JOIN #RTORecoveryPoints rpNextFull ON rp.database_guid = rpNextFull.database_guid AND rp.database_name = rpNextFull.database_name - AND rp.full_last_lsn < rpNextFull.full_last_lsn - LEFT OUTER JOIN #RTORecoveryPoints rpEarlierFull ON rp.database_guid = rpEarlierFull.database_guid AND rp.database_name = rpEarlierFull.database_name - AND rp.full_last_lsn < rpEarlierFull.full_last_lsn - AND rpNextFull.full_last_lsn > rpEarlierFull.full_last_lsn - WHERE rpEarlierFull.full_backup_set_id IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Fill out a diff in that range */ - -RAISERROR('Fill out a diff in that range', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET diff_last_lsn = (SELECT TOP 1 bDiff.last_lsn FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff - WHERE rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name - AND bDiff.type = ''I'' - AND bDiff.last_lsn < rp.log_last_lsn - AND rp.full_backup_set_uuid = bDiff.differential_base_guid - ORDER BY bDiff.last_lsn DESC) - FROM #RTORecoveryPoints rp - WHERE diff_last_lsn IS NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -/* Get time & size totals for full & diff */ - -RAISERROR('Get time & size totals for full & diff', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE #RTORecoveryPoints - SET full_time_seconds = DATEDIFF(ss,bFull.backup_start_date, bFull.backup_finish_date) - , full_file_size_mb = bFull.backup_size / 1048576.0 - , diff_backup_set_id = bDiff.backup_set_id - , diff_time_seconds = DATEDIFF(ss,bDiff.backup_start_date, bDiff.backup_finish_date) - , diff_file_size_mb = bDiff.backup_size / 1048576.0 - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bFull ON rp.database_guid = bFull.database_guid AND rp.database_name = bFull.database_name AND rp.full_last_lsn = bFull.last_lsn - LEFT OUTER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bDiff ON rp.database_guid = bDiff.database_guid AND rp.database_name = bDiff.database_name AND rp.diff_last_lsn = bDiff.last_lsn AND bDiff.last_lsn IS NOT NULL; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - - -/* Get time & size totals for logs */ - -RAISERROR('Get time & size totals for logs', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH LogTotals AS ( - SELECT rp.id, log_time_seconds = SUM(DATEDIFF(ss,bLog.backup_start_date, bLog.backup_finish_date)) - , log_file_size = SUM(bLog.backup_size) - , SUM(1) AS log_backups - FROM #RTORecoveryPoints rp - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupset bLog ON rp.database_guid = bLog.database_guid AND rp.database_name = bLog.database_name AND bLog.type = ''L'' - AND bLog.first_lsn > COALESCE(rp.diff_last_lsn, rp.full_last_lsn) - AND bLog.first_lsn <= rp.log_last_lsn - GROUP BY rp.id - ) - UPDATE #RTORecoveryPoints - SET log_time_seconds = lt.log_time_seconds - , log_file_size_mb = lt.log_file_size / 1048576.0 - , log_backups = lt.log_backups - FROM #RTORecoveryPoints rp - INNER JOIN LogTotals lt ON rp.id = lt.id; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute; - -RAISERROR('Gathering RTO worst cases', 0, 1) WITH NOWAIT; - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH WorstCases AS ( - SELECT rp.* - FROM #RTORecoveryPoints rp - LEFT OUTER JOIN #RTORecoveryPoints rpNewer - ON rp.database_guid = rpNewer.database_guid - AND rp.database_name = rpNewer.database_name - AND rp.full_last_lsn < rpNewer.full_last_lsn - AND rpNewer.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - WHERE rp.rto_worst_case_size_mb = (SELECT TOP 1 rto_worst_case_size_mb FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_size_mb DESC) - /* OR rp.rto_worst_case_time_seconds = (SELECT TOP 1 rto_worst_case_time_seconds FROM #RTORecoveryPoints s WHERE rp.database_guid = s.database_guid AND rp.database_name = s.database_name ORDER BY rto_worst_case_time_seconds DESC) */ - AND rpNewer.database_guid IS NULL - ) - UPDATE #Backups - SET RTOWorstCaseMinutes = - /* Fulls */ - (CASE WHEN @RestoreSpeedFullMBps IS NULL - THEN wc.full_time_seconds / 60.0 - ELSE @RestoreSpeedFullMBps / wc.full_file_size_mb - END) - - /* Diffs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedDiffMBps IS NOT NULL AND wc.diff_file_size_mb IS NOT NULL - THEN @RestoreSpeedDiffMBps / wc.diff_file_size_mb - ELSE COALESCE(wc.diff_time_seconds,0) / 60.0 - END) - - /* Logs, which might not have been taken */ - + (CASE WHEN @RestoreSpeedLogMBps IS NOT NULL AND wc.log_file_size_mb IS NOT NULL - THEN @RestoreSpeedLogMBps / wc.log_file_size_mb - ELSE COALESCE(wc.log_time_seconds,0) / 60.0 - END) - , RTOWorstCaseBackupFileSizeMB = wc.rto_worst_case_size_mb - FROM #Backups b - INNER JOIN WorstCases wc - ON b.database_guid = wc.database_guid - AND b.database_name = wc.database_name; - '; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@RestoreSpeedFullMBps INT, @RestoreSpeedDiffMBps INT, @RestoreSpeedLogMBps INT', @RestoreSpeedFullMBps, @RestoreSpeedDiffMBps, @RestoreSpeedLogMBps; - - - -/*Populating Recoverability*/ - - - /*Get distinct list of databases*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - SELECT DISTINCT b.database_name, database_guid - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b;' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Recoverability ( DatabaseName, DatabaseGUID ) - EXEC sys.sp_executesql @StringToExecute; - - - /*Find most recent recovery model, backup size, and backup date*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.LastBackupRecoveryModel = ca.recovery_model, - r.LastFullBackupSizeMB = ca.compressed_backup_size, - r.LastFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 b.recovery_model, (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date DESC - ) ca;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - /*Find first backup size and date*/ - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.FirstFullBackupSizeMB = ca.compressed_backup_size, - r.FirstFullBackupDate = ca.backup_finish_date - FROM #Recoverability r - CROSS APPLY ( - SELECT TOP 1 (b.compressed_backup_size / 1048576.0) AS compressed_backup_size, b.backup_finish_date - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - ORDER BY b.backup_finish_date ASC - ) ca;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - /*Find average backup throughputs for full, diff, and log*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullBackupThroughputMB = ca_full.AvgFullSpeed, - r.AvgDiffBackupThroughputMB = ca_diff.AvgDiffSpeed, - r.AvgLogBackupThroughputMB = ca_log.AvgLogSpeed, - r.AvgFullBackupDurationSeconds = AvgFullDuration, - r.AvgDiffBackupDurationSeconds = AvgDiffDuration, - r.AvgLogBackupDurationSeconds = AvgLogDuration - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgFullSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgFullDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_full - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgDiffSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgDiffDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_diff - OUTER APPLY ( - SELECT b.database_name, - AVG( b.compressed_backup_size / ( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) / 1048576.0 ) AS AvgLogSpeed, - AVG( DATEDIFF(ss, b.backup_start_date, b.backup_finish_date) ) AS AvgLogDuration - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND DATEDIFF(SECOND, b.backup_start_date, b.backup_finish_date) > 0 - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) ca_log;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - - - /*Find max and avg diff and log sizes*/ - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - UPDATE r - SET r.AvgFullSizeMB = fulls.avg_full_size, - r.AvgDiffSizeMB = diffs.avg_diff_size, - r.AvgLogSizeMB = logs.avg_log_size - FROM #Recoverability AS r - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_full_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''D'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS fulls - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_diff_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''I'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS diffs - OUTER APPLY ( - SELECT b.database_name, AVG(b.compressed_backup_size / 1048576.0) AS avg_log_size - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE r.DatabaseName = b.database_name - AND r.DatabaseGUID = b.database_guid - AND b.type = ''L'' - AND b.backup_finish_date > @StartTime - GROUP BY b.database_name - ) AS logs;' - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -/*Trending - only works if backupfile is populated, which means in msdb */ -IF @MSDBName = N'msdb' -BEGIN - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' --+ @crlf; - - SET @StringToExecute += N' - SELECT p.DatabaseName, - p.DatabaseGUID, - p.[0], - p.[-1], - p.[-2], - p.[-3], - p.[-4], - p.[-5], - p.[-6], - p.[-7], - p.[-8], - p.[-9], - p.[-10], - p.[-11], - p.[-12] - FROM ( SELECT b.database_name AS DatabaseName, - b.database_guid AS DatabaseGUID, - DATEDIFF(MONTH, @StartTime, b.backup_start_date) AS MonthsAgo , - CONVERT(DECIMAL(18, 2), AVG(bf.file_size / 1048576.0)) AS AvgSizeMB - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - INNER JOIN ' + QUOTENAME(@MSDBName) + N'.dbo.backupfile AS bf - ON b.backup_set_id = bf.backup_set_id - WHERE b.database_name NOT IN ( ''master'', ''msdb'', ''model'', ''tempdb'' ) - AND bf.file_type = ''D'' - AND b.backup_start_date >= DATEADD(YEAR, -1, @StartTime) - AND b.backup_start_date <= SYSDATETIME() - GROUP BY b.database_name, - b.database_guid, - DATEDIFF(mm, @StartTime, b.backup_start_date) - ) AS bckstat PIVOT ( SUM(bckstat.AvgSizeMB) FOR bckstat.MonthsAgo IN ( [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) ) AS p - ORDER BY p.DatabaseName; - ' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Trending ( DatabaseName, DatabaseGUID, [0], [-1], [-2], [-3], [-4], [-5], [-6], [-7], [-8], [-9], [-10], [-11], [-12] ) - EXEC sys.sp_executesql @StringToExecute, N'@StartTime DATETIME2', @StartTime; - -END - -/*End Trending*/ - -/*End populating Recoverability*/ - -RAISERROR('Returning data', 0, 1) WITH NOWAIT; - - SELECT b.* - FROM #Backups AS b - ORDER BY b.database_name; - - SELECT r.*, - t.[0], t.[-1], t.[-2], t.[-3], t.[-4], t.[-5], t.[-6], t.[-7], t.[-8], t.[-9], t.[-10], t.[-11], t.[-12] - FROM #Recoverability AS r - LEFT JOIN #Trending t - ON r.DatabaseName = t.DatabaseName - AND r.DatabaseGUID = t.DatabaseGUID - WHERE r.LastBackupRecoveryModel IS NOT NULL - ORDER BY r.DatabaseName - - -RAISERROR('Rules analysis starting', 0, 1) WITH NOWAIT; - -/*Looking for out of band backups by finding most common backup operator user_name and noting backups taken by other user_names*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - WITH common_people AS ( - SELECT TOP 1 b.user_name, COUNT_BIG(*) AS Records - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.user_name - ORDER BY Records DESC - ) - SELECT - 1 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Non-Agent backups taken'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up by '' + QUOTENAME(b.user_name) + '' '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.user_name NOT LIKE ''%Agent%'' AND b.user_name NOT LIKE ''%AGENT%'' - AND NOT EXISTS ( - SELECT 1 - FROM common_people AS cp - WHERE cp.user_name = b.user_name - ) - GROUP BY b.database_name, b.user_name - HAVING COUNT(*) > 1;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings (CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for compatibility level changing. Only looking for databases that have changed more than twice (It''s possible someone may have changed up, had CE problems, and then changed back)*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 2 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Compatibility level changing'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed compatibility levels '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.compatibility_level)) + '' times.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.compatibility_level) > 2;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for password protected backups. This hasn''t been a popular option ever, and was largely replaced by encrypted backups, but it''s simple to check for.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 3 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Password backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up with a password '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times. Who has the password?'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_password_protected = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for snapshot backups. There are legit reasons for these, but we should flag them so the questions get asked. What questions? Good question.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 4 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Snapshot backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' snapshot backups. This message is purely informational.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_snapshot = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*It''s fine to take backups of read only databases, but it''s not always necessary (there''s no new data, after all).*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 5 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Read only state backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in a read-only state. This can be normal if it''''s a secondary, but a bit odd otherwise.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_readonly = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*So, I''ve come across people who think they need to change their database to single user mode to take a backup. Or that doing that will help something. I just need to know, here.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 6 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Single user mode backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times while in single-user mode. This is really weird! Make sure your backup process doesn''''t include a mode change anywhere.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_single_user = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*C''mon, it''s 2017. Take your backups with CHECKSUMS, people.*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 7 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''No CHECKSUMS'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has been backed up '' + CONVERT(VARCHAR(10), COUNT(*)) + '' times without CHECKSUMS in the past 30 days. CHECKSUMS can help alert you to corruption errors.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.has_backup_checksums = 0 - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Damaged is a Black Flag album. You don''t want your backups to be like a Black Flag album. */ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 8 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Damaged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' damaged backups taken without stopping to throw an error. This is done by specifying CONTINUE_AFTER_ERROR in your BACKUP commands.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.is_damaged = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Checking for encrypted backups and the last backup of the encryption key.*/ - - /*2014 ONLY*/ - -IF @ProductVersionMajor >= 12 - BEGIN - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 9 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Encrypted backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' '' + b.encryptor_type + '' backups, and the last time a certificate was backed up is ' - + CASE WHEN LOWER(@MSDBName) <> N'msdb' - THEN + N'...well, that information is on another server, anyway.'' AS [Warning]' - ELSE + CONVERT(VARCHAR(30), (SELECT MAX(c.pvt_key_last_backup_date) FROM sys.certificates AS c WHERE c.name NOT LIKE '##%')) + N'.'' AS [Warning]' - END + - N' - FROM ' + QUOTENAME(@MSDBName) + N'.dbo.backupset AS b - WHERE b.encryptor_type IS NOT NULL - GROUP BY b.database_name, b.encryptor_type;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - END - - /*Looking for backups that have BULK LOGGED data in them -- this can screw up point in time LOG recovery.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 10 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Bulk logged backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' backups with bulk logged data. This can make point in time recovery awkward. '' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.has_bulk_logged_data = 1 - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for recovery model being switched between FULL and SIMPLE, because it''s a bad practice.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 11 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Recovery model switched'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has changed recovery models from between FULL and SIMPLE '' + CONVERT(VARCHAR(10), COUNT(DISTINCT b.recovery_model)) + '' times. This breaks the log chain and is generally a bad idea.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE b.recovery_model <> ''BULK-LOGGED'' - GROUP BY b.database_name - HAVING COUNT(DISTINCT b.recovery_model) > 4;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - - /*Looking for uncompressed backups.*/ - - SET @StringToExecute =N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT - 12 AS CheckId, - 100 AS [Priority], - b.database_name AS [Database Name], - ''Uncompressed backups'' AS [Finding], - ''The database '' + QUOTENAME(b.database_name) + '' has had '' + CONVERT(VARCHAR(10), COUNT(*)) + '' uncompressed backups in the last 30 days. This is a free way to save time and space. And SPACETIME. If your version of SQL supports it.'' AS [Warning] - FROM ' + QUOTENAME(@MSDBName) + '.dbo.backupset AS b - WHERE backup_size = compressed_backup_size AND type = ''D'' - AND b.backup_finish_date >= DATEADD(DAY, -30, SYSDATETIME()) - GROUP BY b.database_name;' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - EXEC sys.sp_executesql @StringToExecute; - -RAISERROR('Rules analysis starting on temp tables', 0, 1) WITH NOWAIT; - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Diffs' AS [Finding], - 'On average, Differential backups for this database are >=40% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigDiff = 1 - - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - SELECT - 13 AS CheckId, - 100 AS Priority, - r.DatabaseName as [DatabaseName], - 'Big Logs' AS [Finding], - 'On average, Log backups for this database are >=20% of the size of the average Full backup.' AS [Warning] - FROM #Recoverability AS r - WHERE r.IsBigLog = 1 - - - -/*Insert thank you stuff last*/ - INSERT #Warnings ( CheckId, Priority, DatabaseName, Finding, Warning ) - - SELECT - 2147483647 AS [CheckId], - 2147483647 AS [Priority], - 'From Your Community Volunteers' AS [DatabaseName], - 'sp_BlitzBackups Version: ' + @Version + ', Version Date: ' + CONVERT(VARCHAR(30), @VersionDate) + '.' AS [Finding], - 'Thanks for using our stored procedure. We hope you find it useful! Check out our other free SQL Server scripts at firstresponderkit.org!' AS [Warning]; - -RAISERROR('Rules analysis finished', 0, 1) WITH NOWAIT; - -SELECT w.CheckId, w.Priority, w.DatabaseName, w.Finding, w.Warning -FROM #Warnings AS w -ORDER BY w.Priority, w.CheckId; - -DROP TABLE #Backups, #Warnings, #Recoverability, #RTORecoveryPoints - - -RETURN; - -PushBackupHistoryToListener: - -RAISERROR('Pushing backup history to listener', 0, 1) WITH NOWAIT; - -DECLARE @msg NVARCHAR(4000) = N''; -DECLARE @RemoteCheck TABLE (c INT NULL); - - -IF @WriteBackupsToDatabaseName IS NULL - BEGIN - RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 0, 1) WITH NOWAIT - RETURN; - END - -IF LOWER(@WriteBackupsToDatabaseName) = N'msdb' - BEGIN - RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 0, 1) WITH NOWAIT - RETURN; - END - -IF @WriteBackupsToListenerName IS NULL -BEGIN - IF @AGName IS NULL - BEGIN - RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 0, 1) WITH NOWAIT; - RETURN; - END - ELSE - BEGIN - SELECT @WriteBackupsToListenerName = dns_name - FROM sys.availability_groups AS ag - JOIN sys.availability_group_listeners AS agl - ON ag.group_id = agl.group_id - WHERE name = @AGName; - END - -END - -IF @WriteBackupsToListenerName IS NOT NULL -BEGIN - IF NOT EXISTS - ( - SELECT * - FROM sys.servers s - WHERE name = @WriteBackupsToListenerName - ) - BEGIN - SET @msg = N'We need a linked server to write data across. Please set one up for ' + @WriteBackupsToListenerName + N'.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - RETURN; - END -END - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + N'.master.sys.databases d WHERE d.name = @i_WriteBackupsToDatabaseName;' - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsToDatabaseName NVARCHAR(256)', @i_WriteBackupsToDatabaseName = @WriteBackupsToDatabaseName; - - IF @@ROWCOUNT = 0 - BEGIN - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to exist on that server.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RETURN; - END - - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'SELECT TOP 1 1 FROM ' - + QUOTENAME(@WriteBackupsToListenerName) + '.' + QUOTENAME(@WriteBackupsToDatabaseName) + '.sys.tables WHERE name = ''backupset'' AND SCHEMA_NAME(schema_id) = ''dbo''; - ' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - INSERT @RemoteCheck (c) - EXEC sp_executesql @StringToExecute; - - IF @@ROWCOUNT = 0 - BEGIN - - SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to have a table called dbo.backupset in it.' - RAISERROR(@msg, 0, 1) WITH NOWAIT - RAISERROR('Don''t worry, we''ll create it for you!', 0, 1) WITH NOWAIT - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'CREATE TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ( backup_set_id INT IDENTITY(1, 1), backup_set_uuid UNIQUEIDENTIFIER, media_set_id INT, first_family_number TINYINT, first_media_number SMALLINT, - last_family_number TINYINT, last_media_number SMALLINT, catalog_family_number TINYINT, catalog_media_number SMALLINT, position INT, expiration_date DATETIME, - software_vendor_id INT, name NVARCHAR(128), description NVARCHAR(255), user_name NVARCHAR(128), software_major_version TINYINT, software_minor_version TINYINT, - software_build_version SMALLINT, time_zone SMALLINT, mtf_minor_version TINYINT, first_lsn NUMERIC(25, 0), last_lsn NUMERIC(25, 0), checkpoint_lsn NUMERIC(25, 0), - database_backup_lsn NUMERIC(25, 0), database_creation_date DATETIME, backup_start_date DATETIME, backup_finish_date DATETIME, type CHAR(1), sort_order SMALLINT, - code_page SMALLINT, compatibility_level TINYINT, database_version INT, backup_size NUMERIC(20, 0), database_name NVARCHAR(128), server_name NVARCHAR(128), - machine_name NVARCHAR(128), flags INT, unicode_locale INT, unicode_compare_style INT, collation_name NVARCHAR(128), is_password_protected BIT, recovery_model NVARCHAR(60), - has_bulk_logged_data BIT, is_snapshot BIT, is_readonly BIT, is_single_user BIT, has_backup_checksums BIT, is_damaged BIT, begins_log_chain BIT, has_incomplete_metadata BIT, - is_force_offline BIT, is_copy_only BIT, first_recovery_fork_guid UNIQUEIDENTIFIER, last_recovery_fork_guid UNIQUEIDENTIFIER, fork_point_lsn NUMERIC(25, 0), database_guid UNIQUEIDENTIFIER, - family_guid UNIQUEIDENTIFIER, differential_base_lsn NUMERIC(25, 0), differential_base_guid UNIQUEIDENTIFIER, compressed_backup_size NUMERIC(20, 0), key_algorithm NVARCHAR(32), - encryptor_thumbprint VARBINARY(20) , encryptor_type NVARCHAR(32) - ); - ' + @crlf; - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - RAISERROR('We''ll even make the indexes!', 0, 1) WITH NOWAIT - - /*Checking for and creating the PK/CX*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - - IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name LIKE ? - ) - - BEGIN - ALTER TABLE ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ADD PRIMARY KEY CLUSTERED ([backup_set_id] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''PK[_][_]%'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on backup_set_uuid*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetuuid] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_set_uuid] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetuuid'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on media_set_id*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += 'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetMediaSetId] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([media_set_id] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetMediaSetId'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on backup_finish_date*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDate] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([backup_finish_date] ASC) - END - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDate'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - - - /*Checking for and creating index on database_name*/ - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N'IF NOT EXISTS ( - SELECT t.name, i.name - FROM ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.tables AS t - JOIN ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.sys.indexes AS i - ON t.object_id = i.object_id - WHERE t.name = ? - AND i.name = ? - ) - - BEGIN - CREATE NONCLUSTERED INDEX [backupsetDatabaseName] ON ' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.[dbo].[backupset] ([database_name] ASC ) INCLUDE ([backup_set_id], [media_set_id]) - END - - ' - - SET @InnerStringToExecute = N'EXEC( ''' + @StringToExecute + ''', ''backupset'', ''backupsetDatabaseName'' ) AT ' + QUOTENAME(@WriteBackupsToListenerName) + N';' - - IF @Debug = 1 - PRINT @InnerStringToExecute; - - EXEC sp_executesql @InnerStringToExecute - - RAISERROR('Table and indexes created! You''re welcome!', 0, 1) WITH NOWAIT - END - - - RAISERROR('Beginning inserts', 0, 1) WITH NOWAIT; - RAISERROR(@crlf, 0, 1) WITH NOWAIT; - - /* - Batching code comes from the lovely and talented Michael J. Swart - http://michaeljswart.com/2014/09/take-care-when-scripting-batches/ - If you're ever in Canada, he says you can stay at his house, too. - */ - - - SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + @crlf; - - SET @StringToExecute += N' - DECLARE - @StartDate DATETIME = DATEADD(HOUR, @i_WriteBackupsLastHours, SYSDATETIME()), - @StartDateNext DATETIME, - @RC INT = 1, - @msg NVARCHAR(4000) = N''''; - - SELECT @StartDate = MIN(b.backup_start_date) - FROM msdb.dbo.backupset b - WHERE b.backup_start_date >= @StartDate - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) - - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - - IF - ( @StartDate IS NULL ) - BEGIN - SET @msg = N''No data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - RETURN; - END - - RAISERROR(''Starting insert loop'', 0, 1) WITH NOWAIT; - - WHILE EXISTS ( - SELECT 1 - FROM msdb.dbo.backupset b - WHERE NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - ) - ) - BEGIN - - SET @msg = N''Inserting data for '' + CONVERT(NVARCHAR(30), @StartDate) + '' through '' + + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - ' - - SET @StringToExecute += N'INSERT ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset - ' - SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data)' + @crlf - ELSE + N'has_bulk_logged_data)' + @crlf - END - - SET @StringToExecute +=N' - SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, - compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, - is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 - THEN + N'encryptor_type, has_bulk_logged_data' + @crlf - ELSE + N'has_bulk_logged_data' + @crlf - END - SET @StringToExecute +=N' - FROM msdb.dbo.backupset b - WHERE 1=1 - AND b.backup_start_date >= @StartDate - AND b.backup_start_date < @StartDateNext - AND NOT EXISTS ( - SELECT 1 - FROM ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset b2 - WHERE b.backup_set_uuid = b2.backup_set_uuid - AND b2.backup_start_date >= @StartDate - )' + @crlf; - - - SET @StringToExecute +=N' - SET @RC = @@ROWCOUNT; - - SET @msg = N''Inserted '' + CONVERT(NVARCHAR(30), @RC) + '' rows for ''+ CONVERT(NVARCHAR(30), @StartDate) + '' through '' + CONVERT(NVARCHAR(30), @StartDateNext) + ''.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - SET @StartDate = @StartDateNext; - SET @StartDateNext = DATEADD(MINUTE, 10, @StartDate); - - IF - ( @StartDate > SYSDATETIME() ) - BEGIN - - SET @msg = N''No more data to move, exiting.'' - RAISERROR(@msg, 0, 1) WITH NOWAIT - - BREAK; - - END - END' + @crlf; - - IF @Debug = 1 - PRINT @StringToExecute; - - EXEC sp_executesql @StringToExecute, N'@i_WriteBackupsLastHours INT', @i_WriteBackupsLastHours = @WriteBackupsLastHours; - -END; - -END; - -GO -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @msg VARCHAR(8000); - SELECT @msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; -END; - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); -GO - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheProcs;'); -GO - -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheResults;'); -GO - -CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) -); - -CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - /*The Memory Grant columns are only supported - in certain versions, giggle giggle. - */ - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); -GO - -ALTER PROCEDURE dbo.sp_BlitzCache - @Help BIT = 0, - @Top INT = NULL, - @SortOrder VARCHAR(50) = 'CPU', - @UseTriggersAnyway BIT = NULL, - @ExportToExcel BIT = 0, - @ExpertMode TINYINT = 0, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @ConfigurationDatabaseName NVARCHAR(128) = NULL , - @ConfigurationSchemaName NVARCHAR(256) = NULL , - @ConfigurationTableName NVARCHAR(256) = NULL , - @DurationFilter DECIMAL(38,4) = NULL , - @HideSummary BIT = 0 , - @IgnoreSystemDBs BIT = 1 , - @OnlyQueryHashes VARCHAR(MAX) = NULL , - @IgnoreQueryHashes VARCHAR(MAX) = NULL , - @OnlySqlHandles VARCHAR(MAX) = NULL , - @IgnoreSqlHandles VARCHAR(MAX) = NULL , - @QueryFilter VARCHAR(10) = 'ALL' , - @DatabaseName NVARCHAR(128) = NULL , - @StoredProcName NVARCHAR(128) = NULL, - @Reanalyze BIT = 0 , - @SkipAnalysis BIT = 0 , - @BringThePain BIT = 0, /* This will forcibly set @Top to 2,147,483,647 */ - @MinimumExecutionCount INT = 0, - @Debug BIT = 0, - @CheckDateOverride DATETIMEOFFSET = NULL, - @MinutesBack INT = NULL, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - -IF @Help = 1 PRINT ' -sp_BlitzCache from http://FirstResponderKit.org - -This script displays your most resource-intensive queries from the plan cache, -and points to ways you can tune these queries to make them faster. - - -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. - -Known limitations of this version: - - This query will not run on SQL Server 2005. - - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is - excluded by default. - - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes - with no spaces between the hash values. - - @OutputServerName is not functional yet. - -Unknown limitations of this version: - - May or may not be vulnerable to the wick effect. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -'; - -DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; - -IF @Help = 1 -BEGIN - SELECT N'@Help' AS [Parameter Name] , - N'BIT' AS [Data Type] , - N'Displays this help message.' AS [Parameter Description] - - UNION ALL - SELECT N'@Top', - N'INT', - N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' - - UNION ALL - SELECT N'@SortOrder', - N'VARCHAR(10)', - N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' - - UNION ALL - SELECT N'@UseTriggersAnyway', - N'BIT', - N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' - - UNION ALL - SELECT N'@ExportToExcel', - N'BIT', - N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' - - UNION ALL - SELECT N'@ExpertMode', - N'TINYINT', - N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' - - UNION ALL - SELECT N'@OutputDatabaseName', - N'NVARCHAR(128)', - N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputSchemaName', - N'NVARCHAR(256)', - N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' - - UNION ALL - SELECT N'@OutputTableName', - N'NVARCHAR(256)', - N'The output table. If this does not exist, it will be created for you.' - - UNION ALL - SELECT N'@DurationFilter', - N'DECIMAL(38,4)', - N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' - - UNION ALL - SELECT N'@HideSummary', - N'BIT', - N'Hides the findings summary result set.' - - UNION ALL - SELECT N'@IgnoreSystemDBs', - N'BIT', - N'Ignores plans found in the system databases (master, model, msdb, tempdb, and resourcedb)' - - UNION ALL - SELECT N'@OnlyQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' - - UNION ALL - SELECT N'@IgnoreQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to ignore.' - - UNION ALL - SELECT N'@OnlySqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to use for filtering results.' - - UNION ALL - SELECT N'@IgnoreSqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to ignore.' - - UNION ALL - SELECT N'@DatabaseName', - N'NVARCHAR(128)', - N'A database name which is used for filtering results.' - - UNION ALL - SELECT N'@StoredProcName', - N'NVARCHAR(128)', - N'Name of stored procedure you want to find plans for.' - - UNION ALL - SELECT N'@BringThePain', - N'BIT', - N'This forces sp_BlitzCache to examine the entire plan cache. Be careful running this on servers with a lot of memory or a large execution plan cache.' - - UNION ALL - SELECT N'@QueryFilter', - N'VARCHAR(10)', - N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' - - UNION ALL - SELECT N'@Reanalyze', - N'BIT', - N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' - - UNION ALL - SELECT N'@MinimumExecutionCount', - N'INT', - N'Queries with fewer than this number of executions will be omitted from results.' - - UNION ALL - SELECT N'@Debug', - N'BIT', - N'Setting this to 1 will print dynamic SQL and select data from all tables used.' - - UNION ALL - SELECT N'@MinutesBack', - N'INT', - N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negtive.'; - - - /* Column definitions */ - SELECT N'# Executions' AS [Column Name], - N'BIGINT' AS [Data Type], - N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] - - UNION ALL - SELECT N'Executions / Minute', - N'MONEY', - N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' - - UNION ALL - SELECT N'Execution Weight', - N'MONEY', - N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Database', - N'sysname', - N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' - - UNION ALL - SELECT N'Total CPU', - N'BIGINT', - N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' - - UNION ALL - SELECT N'Avg CPU', - N'BIGINT', - N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'CPU Weight', - N'MONEY', - N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Duration', - N'BIGINT', - N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' - - UNION ALL - SELECT N'Avg Duration', - N'BIGINT', - N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'Duration Weight', - N'MONEY', - N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Reads', - N'BIGINT', - N'Total logical reads performed by this query since last compilation.' - - UNION ALL - SELECT N'Average Reads', - N'BIGINT', - N'Average logical reads performed by each execution of this query since the last compilation.' - - UNION ALL - SELECT N'Read Weight', - N'MONEY', - N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Total Writes', - N'BIGINT', - N'Total logical writes performed by this query since last compilation.' - - UNION ALL - SELECT N'Average Writes', - N'BIGINT', - N'Average logical writes performed by each execution this query since last compilation.' - - UNION ALL - SELECT N'Write Weight', - N'MONEY', - N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' - - UNION ALL - SELECT N'Query Type', - N'NVARCHAR(256)', - N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' - - UNION ALL - SELECT N'Query Text', - N'NVARCHAR(4000)', - N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' - - UNION ALL - SELECT N'% Executions (Type)', - N'MONEY', - N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' - - UNION ALL - SELECT N'% CPU (Type)', - N'MONEY', - N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' - - UNION ALL - SELECT N'% Duration (Type)', - N'MONEY', - N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' - - UNION ALL - SELECT N'% Reads (Type)', - N'MONEY', - N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' - - UNION ALL - SELECT N'% Writes (Type)', - N'MONEY', - N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' - - UNION ALL - SELECT N'Total Rows', - N'BIGINT', - N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' - - UNION ALL - SELECT N'Average Rows', - N'MONEY', - N'Average number of rows returned by each execution of the query.' - - UNION ALL - SELECT N'Min Rows', - N'BIGINT', - N'The minimum number of rows returned by any execution of this query.' - - UNION ALL - SELECT N'Max Rows', - N'BIGINT', - N'The maximum number of rows returned by any execution of this query.' - - UNION ALL - SELECT N'MinGrantKB', - N'BIGINT', - N'The minimum memory grant the query received in kb.' - - UNION ALL - SELECT N'MaxGrantKB', - N'BIGINT', - N'The maximum memory grant the query received in kb.' - - UNION ALL - SELECT N'MinUsedGrantKB', - N'BIGINT', - N'The minimum used memory grant the query received in kb.' - - UNION ALL - SELECT N'MaxUsedGrantKB', - N'BIGINT', - N'The maximum used memory grant the query received in kb.' - - UNION ALL - SELECT N'PercentMemoryGrantUsed', - N'MONEY', - N'Result of dividing the maximum grant used by the minimum granted.' - - UNION ALL - SELECT N'AvgMaxMemoryGrant', - N'MONEY', - N'The average maximum memory grant for a query.' - - UNION ALL - SELECT N'# Plans', - N'INT', - N'The total number of execution plans found that match a given query.' - - UNION ALL - SELECT N'# Distinct Plans', - N'INT', - N'The number of distinct execution plans that match a given query. ' - + NCHAR(13) + NCHAR(10) - + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' - - UNION ALL - SELECT N'Created At', - N'DATETIME', - N'Time that the execution plan was last compiled.' - - UNION ALL - SELECT N'Last Execution', - N'DATETIME', - N'The last time that this query was executed.' - - UNION ALL - SELECT N'Query Plan', - N'XML', - N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' - - UNION ALL - SELECT N'Plan Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to the compiled plan this query is a part of.' - - UNION ALL - SELECT N'SQL Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' - - UNION ALL - SELECT N'Query Hash', - N'BINARY(8)', - N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' - - UNION ALL - SELECT N'Warnings', - N'VARCHAR(MAX)', - N'A list of individual warnings generated by this query.' ; - - - - /* Configuration table description */ - SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , - N'100' AS [Default Value] , - N'Executions / Minute' AS [Unit of Measure] , - N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] - - UNION ALL - SELECT N'Parameter Sniffing Variance Percent' , - N'30' , - N'Percent' , - N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' - - UNION ALL - SELECT N'Parameter Sniffing IO Threshold' , - N'100,000' , - N'Logical reads' , - N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' - - UNION ALL - SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' - - UNION ALL - SELECT N'Long Running Query Warning' AS [Configuration Parameter] , - N'300' , - N'Seconds' , - N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' - - UNION ALL - SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; - RETURN; -END; - -/*Validate version*/ -IF ( -SELECT - CASE - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '8%' THEN 0 - WHEN CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')) LIKE '9%' THEN 0 - ELSE 1 - END -) = 0 -BEGIN - DECLARE @version_msg VARCHAR(8000); - SELECT @version_msg = 'Sorry, sp_BlitzCache doesn''t work on versions of SQL prior to 2008.' + REPLICATE(CHAR(13), 7933); - PRINT @version_msg; - RETURN; -END; - -/* Set @Top based on sort */ -IF ( - @Top IS NULL - AND LOWER(@SortOrder) IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 5; - END; - -IF ( - @Top IS NULL - AND LOWER(@SortOrder) NOT IN ( 'all', 'all sort' ) - ) - BEGIN - SET @Top = 10; - END; - -/* validate user inputs */ -IF @Top IS NULL - OR @SortOrder IS NULL - OR @QueryFilter IS NULL - OR @Reanalyze IS NULL -BEGIN - RAISERROR(N'Several parameters (@Top, @SortOrder, @QueryFilter, @renalyze) are required. Do not set them to NULL. Please try again.', 16, 1) WITH NOWAIT; - RETURN; -END; - -RAISERROR(N'Checking @MinutesBack validity.', 0, 1) WITH NOWAIT; -IF @MinutesBack IS NOT NULL - BEGIN - IF @MinutesBack > 0 - BEGIN - RAISERROR(N'Setting @MinutesBack to a negative number', 0, 1) WITH NOWAIT; - SET @MinutesBack *=-1; - END; - IF @MinutesBack = 0 - BEGIN - RAISERROR(N'@MinutesBack can''t be 0, setting to -1', 0, 1) WITH NOWAIT; - SET @MinutesBack = -1; - END; - END; - - -RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; - -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults') IS NULL -BEGIN - CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) - ); -END; - -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs') IS NULL -BEGIN - CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); -END; - -DECLARE @DurationFilter_i INT, - @MinMemoryPerQuery INT, - @msg NVARCHAR(4000) ; - - -IF @BringThePain = 1 - BEGIN - RAISERROR(N'You have chosen to bring the pain. Setting top to 2147483647.', 0, 1) WITH NOWAIT; - SET @Top = 2147483647; - END; - -/* Change duration from seconds to milliseconds */ -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; - SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); - END; - -RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; -SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; -IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> '' -BEGIN - RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); - RETURN; -END; -IF (SELECT DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' -BEGIN - RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); - RETURN; -END; - -SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; - -SET @SortOrder = LOWER(@SortOrder); -SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); -SET @SortOrder = REPLACE(@SortOrder, 'executions per minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'executions / minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'xpm', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'recent compilations', 'compiles'); - -RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; -IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', - 'duration', 'avg duration', 'executions', 'avg executions', - 'compiles', 'memory grant', 'avg memory grant', - 'all', 'all avg') - BEGIN - RAISERROR(N'Invalid sort order chosen, reverting to cpu', 0, 1) WITH NOWAIT; - SET @SortOrder = 'cpu'; - END; - -SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); - -SET @QueryFilter = LOWER(@QueryFilter); - -IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') - BEGIN - RAISERROR(N'Invalid query filter chosen. Reverting to all.', 0, 1) WITH NOWAIT; - SET @QueryFilter = 'all'; - END; - -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skip Analysis set to 1, hiding Summary', 0, 1) WITH NOWAIT; - SET @HideSummary = 1; - END; - -IF @Reanalyze = 1 AND OBJECT_ID('tempdb..##bou_BlitzCacheResults') IS NULL - BEGIN - RAISERROR(N'##bou_BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; - SET @Reanalyze = 0; - END; - -IF @Reanalyze = 0 - BEGIN - RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheResults - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - END; - -IF @Reanalyze = 1 - BEGIN - RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; - GOTO Results; - END; - -IF @SortOrder IN ('all', 'all avg') - BEGIN - RAISERROR(N'Checking all sort orders, please be patient', 0, 1) WITH NOWAIT; - GOTO AllSorts; - END; - - -RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; -IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL - DROP TABLE #only_query_hashes ; - -IF OBJECT_ID('tempdb..#ignore_query_hashes') IS NOT NULL - DROP TABLE #ignore_query_hashes ; - -IF OBJECT_ID('tempdb..#only_sql_handles') IS NOT NULL - DROP TABLE #only_sql_handles ; - -IF OBJECT_ID('tempdb..#ignore_sql_handles') IS NOT NULL - DROP TABLE #ignore_sql_handles ; - -IF OBJECT_ID('tempdb..#p') IS NOT NULL - DROP TABLE #p; - -IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL - DROP TABLE #checkversion; - -IF OBJECT_ID ('tempdb..#configuration') IS NOT NULL - DROP TABLE #configuration; - -IF OBJECT_ID ('tempdb..#stored_proc_info') IS NOT NULL - DROP TABLE #stored_proc_info; - -IF OBJECT_ID ('tempdb..#plan_creation') IS NOT NULL - DROP TABLE #plan_creation; - -IF OBJECT_ID ('tempdb..#est_rows') IS NOT NULL - DROP TABLE #est_rows; - -IF OBJECT_ID ('tempdb..#plan_cost') IS NOT NULL - DROP TABLE #plan_cost; - -IF OBJECT_ID ('tempdb..#proc_costs') IS NOT NULL - DROP TABLE #proc_costs; - -IF OBJECT_ID ('tempdb..#stats_agg') IS NOT NULL - DROP TABLE #stats_agg; - -IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL - DROP TABLE #trace_flags; - -IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL - DROP TABLE #variable_info - -IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL - DROP TABLE #conversion_info - - -IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL - DROP TABLE #missing_index_xml - -IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL - DROP TABLE #missing_index_schema - -IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL - DROP TABLE #missing_index_usage - -IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL - DROP TABLE #missing_index_detail - -IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL - DROP TABLE #missing_index_pretty - - -CREATE TABLE #only_query_hashes ( - query_hash BINARY(8) -); - -CREATE TABLE #ignore_query_hashes ( - query_hash BINARY(8) -); - -CREATE TABLE #only_sql_handles ( - sql_handle VARBINARY(64) -); - -CREATE TABLE #ignore_sql_handles ( - sql_handle VARBINARY(64) -); - -CREATE TABLE #p ( - SqlHandle VARBINARY(64), - TotalCPU BIGINT, - TotalDuration BIGINT, - TotalReads BIGINT, - TotalWrites BIGINT, - ExecutionCount BIGINT -); - -CREATE TABLE #checkversion ( - version NVARCHAR(128), - common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), - major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), - minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), - build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), - revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) -); - -CREATE TABLE #configuration ( - parameter_name VARCHAR(100), - value DECIMAL(38,0) -); - -CREATE TABLE #stored_proc_info -( - SPID INT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - variable_name NVARCHAR(128), - variable_datatype NVARCHAR(128), - converted_column_name NVARCHAR(128), - compile_time_value NVARCHAR(128), - proc_name NVARCHAR(300), - column_name NVARCHAR(128), - converted_to NVARCHAR(128) -); - -CREATE TABLE #plan_creation -( - percent_24 DECIMAL(5, 2), - percent_4 DECIMAL(5, 2), - percent_1 DECIMAL(5, 2), - total_plans INT, - SPID INT -); - -CREATE TABLE #est_rows -( - QueryHash BINARY(8), - estimated_rows FLOAT -); - -CREATE TABLE #plan_cost -( - QueryPlanCost FLOAT, - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - QueryPlanHash BINARY(8) -); - -CREATE TABLE #proc_costs -( - PlanTotalQuery FLOAT, - PlanHandle VARBINARY(64), - SqlHandle VARBINARY(64) -); - -CREATE TABLE #stats_agg -( - SqlHandle VARBINARY(64), - LastUpdate DATETIME2(7), - ModificationCount INT, - SamplingPercent FLOAT, - [Statistics] NVARCHAR(256), - [Table] NVARCHAR(256), - [Schema] NVARCHAR(256), - [Database] NVARCHAR(256), -); - -CREATE TABLE #trace_flags -( - SqlHandle VARBINARY(64), - QueryHash BINARY(8), - global_trace_flags VARCHAR(1000), - session_trace_flags VARCHAR(1000) -); - -CREATE TABLE #variable_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - variable_name NVARCHAR(200), - variable_datatype NVARCHAR(128), - compile_time_value NVARCHAR(4000) -); - -CREATE TABLE #conversion_info -( - SPID INT, - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - proc_name NVARCHAR(128), - expression NVARCHAR(4000), - at_charindex AS CHARINDEX('@', expression), - bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), - comma_charindex AS CHARINDEX(',', expression) + 1, - second_comma_charindex AS - CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, - equal_charindex AS CHARINDEX('=', expression) + 1, - paren_charindex AS CHARINDEX('(', expression) + 1, - comma_paren_charindex AS - CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, - convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression) -); - - -CREATE TABLE #missing_index_xml -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - index_xml XML -); - - -CREATE TABLE #missing_index_schema -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - index_xml XML -); - - -CREATE TABLE #missing_index_usage -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - index_xml XML -); - - -CREATE TABLE #missing_index_detail -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - column_name NVARCHAR(128) -); - - -CREATE TABLE #missing_index_pretty -( - QueryHash BINARY(8), - SqlHandle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), - details AS N'/* ' - + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' - + CONVERT(NVARCHAR(30), impact) - + '%.' - + CHAR(10) - + N'*/' - + CHAR(10) + CHAR(13) - + N'/* ' - + CHAR(10) - + N'USE ' - + database_name - + CHAR(10) - + N'GO' - + CHAR(10) + CHAR(13) - + N'CREATE NONCLUSTERED INDEX ix_' - + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') - + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END - + CHAR(10) - + N' ON ' - + schema_name - + N'.' - + table_name - + N' (' + - + CASE WHEN equality IS NOT NULL - THEN equality - + CASE WHEN inequality IS NOT NULL - THEN N', ' + inequality - ELSE N'' - END - ELSE inequality - END - + N')' - + CHAR(10) - + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' - END - + CHAR(10) - + N'GO' - + CHAR(10) - + N'*/' -); - -RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; -WITH x AS ( -SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 4 THEN 1 ELSE 0 END) AS [plans_4], - SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 1 THEN 1 ELSE 0 END) AS [plans_1], - COUNT(deqs.creation_time) AS [total_plans] -FROM sys.dm_exec_query_stats AS deqs -) -INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) -SELECT CONVERT(DECIMAL(3,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], - x.total_plans, - @@SPID AS SPID -FROM x -OPTION (RECOMPILE) ; - - -SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; -SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; -SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; - -DECLARE @individual VARCHAR(100) ; - -IF (@OnlySqlHandles IS NOT NULL AND @IgnoreSqlHandles IS NOT NULL) -BEGIN -RAISERROR('You shouldn''t need to ignore and filter on SqlHandle at the same time.', 0, 1) WITH NOWAIT; -RETURN; -END; - -IF (@StoredProcName IS NOT NULL AND (@OnlySqlHandles IS NOT NULL OR @IgnoreSqlHandles IS NOT NULL)) -BEGIN -RAISERROR('You can''t filter on stored procedure name and SQL Handle.', 0, 1) WITH NOWAIT; -RETURN; -END; - -IF @OnlySqlHandles IS NOT NULL - AND LEN(@OnlySqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@OnlySqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @OnlySqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlySqlHandles, 0, PATINDEX('%,%',@OnlySqlHandles)) ; - - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @OnlySqlHandles = SUBSTRING(@OnlySqlHandles, LEN(@individual + ',') + 1, LEN(@OnlySqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @OnlySqlHandles; - SET @OnlySqlHandles = NULL; - - INSERT INTO #only_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -IF @IgnoreSqlHandles IS NOT NULL - AND LEN(@IgnoreSqlHandles) > 0 -BEGIN - RAISERROR(N'Processing SQL Handles To Ignore', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@IgnoreSqlHandles) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreSqlHandles) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreSqlHandles, 0, PATINDEX('%,%',@IgnoreSqlHandles)) ; - - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @IgnoreSqlHandles = SUBSTRING(@IgnoreSqlHandles, LEN(@individual + ',') + 1, LEN(@IgnoreSqlHandles)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreSqlHandles; - SET @IgnoreSqlHandles = NULL; - - INSERT INTO #ignore_sql_handles - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' - -BEGIN - RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; - INSERT #only_sql_handles - ( sql_handle ) - SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) - FROM sys.dm_exec_procedure_stats AS deps - WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName - OPTION (RECOMPILE) ; - - IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 - BEGIN - RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; - RETURN; - END; - -END; - - - -IF ((@OnlyQueryHashes IS NOT NULL AND LEN(@OnlyQueryHashes) > 0) - OR (@IgnoreQueryHashes IS NOT NULL AND LEN(@IgnoreQueryHashes) > 0)) - AND LEFT(@QueryFilter, 3) IN ('pro', 'fun') -BEGIN - RAISERROR('You cannot limit by query hash and filter by stored procedure', 16, 1); - RETURN; -END; - -/* If the user is attempting to limit by query hash, set up the - #only_query_hashes temp table. This will be used to narrow down - results. - - Just a reminder: Using @OnlyQueryHashes will ignore stored - procedures and triggers. - */ -IF @OnlyQueryHashes IS NOT NULL - AND LEN(@OnlyQueryHashes) > 0 -BEGIN - RAISERROR(N'Setting up filter for Query Hashes', 0, 1) WITH NOWAIT; - SET @individual = ''; - - WHILE LEN(@OnlyQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @OnlyQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@OnlyQueryHashes, 0, PATINDEX('%,%',@OnlyQueryHashes)) ; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS BINARY(8)); - - SET @OnlyQueryHashes = SUBSTRING(@OnlyQueryHashes, LEN(@individual + ',') + 1, LEN(@OnlyQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @OnlyQueryHashes; - SET @OnlyQueryHashes = NULL; - - INSERT INTO #only_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - --SELECT CAST(SUBSTRING(@individual, 1, 2) AS VARBINARY(MAX)) ; - END; - END; -END; - -/* If the user is setting up a list of query hashes to ignore, those - values will be inserted into #ignore_query_hashes. This is used to - exclude values from query results. - - Just a reminder: Using @IgnoreQueryHashes will ignore stored - procedures and triggers. - */ -IF @IgnoreQueryHashes IS NOT NULL - AND LEN(@IgnoreQueryHashes) > 0 -BEGIN - RAISERROR(N'Setting up filter to ignore query hashes', 0, 1) WITH NOWAIT; - SET @individual = '' ; - - WHILE LEN(@IgnoreQueryHashes) > 0 - BEGIN - IF PATINDEX('%,%', @IgnoreQueryHashes) > 0 - BEGIN - SET @individual = SUBSTRING(@IgnoreQueryHashes, 0, PATINDEX('%,%',@IgnoreQueryHashes)) ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - - SET @IgnoreQueryHashes = SUBSTRING(@IgnoreQueryHashes, LEN(@individual + ',') + 1, LEN(@IgnoreQueryHashes)) ; - END; - ELSE - BEGIN - SET @individual = @IgnoreQueryHashes ; - SET @IgnoreQueryHashes = NULL ; - - INSERT INTO #ignore_query_hashes - SELECT CAST('' AS XML).value('xs:hexBinary( substring(sql:variable("@individual"), sql:column("t.pos")) )', 'varbinary(max)') - FROM (SELECT CASE SUBSTRING(@individual, 1, 2) WHEN '0x' THEN 3 ELSE 0 END) AS t(pos) - OPTION (RECOMPILE) ; - END; - END; -END; - -IF @ConfigurationDatabaseName IS NOT NULL -BEGIN - RAISERROR(N'Reading values from Configuration Database', 0, 1) WITH NOWAIT; - DECLARE @config_sql NVARCHAR(MAX) = N'INSERT INTO #configuration SELECT parameter_name, value FROM ' - + QUOTENAME(@ConfigurationDatabaseName) - + '.' + QUOTENAME(@ConfigurationSchemaName) - + '.' + QUOTENAME(@ConfigurationTableName) - + ' ; ' ; - EXEC(@config_sql); -END; - -RAISERROR(N'Setting up variables', 0, 1) WITH NOWAIT; -DECLARE @sql NVARCHAR(MAX) = N'', - @insert_list NVARCHAR(MAX) = N'', - @plans_triggers_select_list NVARCHAR(MAX) = N'', - @body NVARCHAR(MAX) = N'', - @body_where NVARCHAR(MAX) = N'WHERE 1 = 1 ' + @nl, - @body_order NVARCHAR(MAX) = N'ORDER BY #sortable# DESC OPTION (RECOMPILE) ', - - @q NVARCHAR(1) = N'''', - @pv VARCHAR(20), - @pos TINYINT, - @v DECIMAL(6,2), - @build INT; - - -RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; - -INSERT INTO #checkversion (version) -SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) -OPTION (RECOMPILE); - - -SELECT @v = common_version , - @build = build -FROM #checkversion -OPTION (RECOMPILE); - -IF (@SortOrder IN ('memory grant', 'avg memory grant')) -AND ((@v < 11) -OR (@v = 11 AND @build < 6020) -OR (@v = 12 AND @build < 5000) -OR (@v = 13 AND @build < 1601)) -BEGIN - RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); - RETURN; -END; - -IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) -BEGIN - RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); - RETURN; -END; - -RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; - -SET @insert_list += N' -INSERT INTO ##bou_BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, - PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, - ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, - LastExecutionTime, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, - LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, - QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, - TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, - min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime) ' ; - -SET @body += N' -FROM (SELECT TOP (@Top) x.*, xpa.*, - CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY) as age_minutes, - CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY) as age_minutes_lifetime - FROM sys.#view# x - CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa - WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; - -SET @body += N' WHERE 1 = 1 ' + @nl ; - - -IF @IgnoreSystemDBs = 1 - BEGIN - RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; - SET @body += N' AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - END; - -IF @DatabaseName IS NOT NULL OR @DatabaseName <> '' - BEGIN - RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; - SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(' - + QUOTENAME(@DatabaseName, N'''') - + N') ' + @nl; - END; - -IF (SELECT COUNT(*) FROM #only_sql_handles) > 0 -BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; - -IF (SELECT COUNT(*) FROM #ignore_sql_handles) > 0 -BEGIN - RAISERROR(N'Including only chosen SQL Handles', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_sql_handles q WHERE q.sql_handle = x.sql_handle) ' + @nl ; -END; - -IF (SELECT COUNT(*) FROM #only_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #only_sql_handles) = 0 - AND (SELECT COUNT(*) FROM #ignore_sql_handles) = 0 -BEGIN - RAISERROR(N'Including only chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND EXISTS(SELECT 1/0 FROM #only_query_hashes q WHERE q.query_hash = x.query_hash) ' + @nl ; -END; - -/* filtering for query hashes */ -IF (SELECT COUNT(*) FROM #ignore_query_hashes) > 0 - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 -BEGIN - RAISERROR(N'Excluding chosen Query Hashes', 0, 1) WITH NOWAIT; - SET @body += N' AND NOT EXISTS(SELECT 1/0 FROM #ignore_query_hashes iq WHERE iq.query_hash = x.query_hash) ' + @nl ; -END; -/* end filtering for query hashes */ - - -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; - SET @body += N' AND (total_elapsed_time / 1000.0) / execution_count > @min_duration ' + @nl ; - END; - -IF @MinutesBack IS NOT NULL - BEGIN - RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; - SET @body += N' AND x.last_execution_time >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; - END; - -/* Apply the sort order here to only grab relevant plans. - This should make it faster to process since we'll be pulling back fewer - plans for processing. - */ -RAISERROR(N'Applying chosen sort order', 0, 1) WITH NOWAIT; -SELECT @body += N' ORDER BY ' + - CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, GETDATE()) - ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 - THEN DATEDIFF(mi, cached_time, last_execution_time) - ELSE Null END) as MONEY))) AS money) - END ' - END + N' DESC ' + @nl ; - - - -SET @body += N') AS qs - CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, - SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, - SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, - SUM(CAST(total_logical_reads AS BIGINT)) AS t_TotalReads, - SUM(CAST(total_logical_writes AS BIGINT)) AS t_TotalWrites - FROM sys.#view#) AS t - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa - CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; - -SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; - - - -SET @plans_triggers_select_list += N' -SELECT TOP (@Top) - @@SPID , - ''Procedure or Function: '' + COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''') AS QueryType, - COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_worker_time / 1000.0) / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * (total_elapsed_time / 1000.0) / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CASE WHEN t.t_TotalExecs = 0 THEN 0 - ELSE CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) - END AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.cached_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.cached_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - NULL AS StatementStartOffset, - NULL AS StatementEndOffset, - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, - st.text AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - NULL AS QueryHash, - NULL AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_elapsed_time / 1000.0, - age_minutes, - age_minutes_lifetime '; - - -IF LEFT(@QueryFilter, 3) IN ('all', 'sta') -BEGIN - SET @sql += @insert_list; - - SET @sql += N' - SELECT TOP (@Top) - @@SPID , - ''Statement'' AS QueryType, - COALESCE(DB_NAME(CAST(pa.value AS INT)), ''-- N/A --'') AS DatabaseName, - (total_worker_time / 1000.0) / execution_count AS AvgCPU , - (total_worker_time / 1000.0) AS TotalCPU , - CASE WHEN total_worker_time = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((total_worker_time / 1000.0) / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time)) AS MONEY) - END AS AverageCPUPerMinute , - CASE WHEN t.t_TotalWorker = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_worker_time / t.t_TotalWorker, 2) AS MONEY) - END AS PercentCPUByType, - CASE WHEN t.t_TotalElapsed = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_elapsed_time / t.t_TotalElapsed, 2) AS MONEY) - END AS PercentDurationByType, - CASE WHEN t.t_TotalReads = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_reads / t.t_TotalReads, 2) AS MONEY) - END AS PercentReadsByType, - CAST(ROUND(100.00 * execution_count / t.t_TotalExecs, 2) AS MONEY) AS PercentExecutionsByType, - (total_elapsed_time / 1000.0) / execution_count AS AvgDuration , - (total_elapsed_time / 1000.0) AS TotalDuration , - total_logical_reads / execution_count AS AvgReads , - total_logical_reads AS TotalReads , - execution_count AS ExecutionCount , - CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time))) AS money) - END AS ExecutionsPerMinute , - total_logical_writes AS TotalWrites , - total_logical_writes / execution_count AS AverageWrites , - CASE WHEN t.t_TotalWrites = 0 THEN 0 - ELSE CAST(ROUND(100.00 * total_logical_writes / t.t_TotalWrites, 2) AS MONEY) - END AS PercentWritesByType, - CASE WHEN total_logical_writes = 0 THEN 0 - WHEN COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0) = 0 THEN 0 - ELSE CAST((1.00 * total_logical_writes / COALESCE(age_minutes, DATEDIFF(mi, qs.creation_time, qs.last_execution_time), 0)) AS money) - END AS WritesPerMinute, - qs.creation_time AS PlanCreationTime, - qs.last_execution_time AS LastExecutionTime, - qs.statement_start_offset AS StatementStartOffset, - qs.statement_end_offset AS StatementEndOffset, '; - - IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) - BEGIN - RAISERROR(N'Adding additional info columns for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - qs.min_rows AS MinReturnedRows, - qs.max_rows AS MaxReturnedRows, - CAST(qs.total_rows as MONEY) / execution_count AS AvgReturnedRows, - qs.total_rows AS TotalReturnedRows, - qs.last_rows AS LastReturnedRows, ' ; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for more info columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinReturnedRows, - NULL AS MaxReturnedRows, - NULL AS AvgReturnedRows, - NULL AS TotalReturnedRows, - NULL AS LastReturnedRows, ' ; - END; - - IF (@v = 11 AND @build >= 6020) OR (@v = 12 AND @build >= 5000) OR (@v = 13 AND @build >= 1601) - - BEGIN - RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - min_grant_kb AS MinGrantKB, - max_grant_kb AS MaxGrantKB, - min_used_grant_kb AS MinUsedGrantKB, - max_used_grant_kb AS MaxUsedGrantKB, - CAST(ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, - CAST(ISNULL(NULLIF(( max_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; - END; - ELSE - BEGIN - RAISERROR(N'Substituting NULLs for memory grant columns in older versions of SQL', 0, 1) WITH NOWAIT; - SET @sql += N' - NULL AS MinGrantKB, - NULL AS MaxGrantKB, - NULL AS MinUsedGrantKB, - NULL AS MaxUsedGrantKB, - NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, ' ; - END; - - - SET @sql += N' - SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qs.statement_end_offset - END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, - t.t_TotalReads, - t.t_TotalExecs, - t.t_TotalWrites, - qs.sql_handle AS SqlHandle, - qs.plan_handle AS PlanHandle, - qs.query_hash AS QueryHash, - qs.query_plan_hash AS QueryPlanHash, - qs.min_worker_time / 1000.0, - qs.max_worker_time / 1000.0, - CASE WHEN qp.query_plan.value(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";max(//p:RelOp/@Parallel)'', ''float'') > 0 THEN 1 ELSE 0 END, - qs.min_elapsed_time / 1000.0, - qs.max_worker_time / 1000.0, - age_minutes, - age_minutes_lifetime '; - - SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; - - SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; - - SET @sql += @body_order + @nl + @nl + @nl; - - IF @SortOrder = 'compiles' - BEGIN - RAISERROR(N'Sorting by compiles', 0, 1) WITH NOWAIT; - SET @sql = REPLACE(@sql, '#sortable#', 'creation_time'); - END; -END; - - -IF (@QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'pro') -BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Stored Procedure') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_procedure_stats') ; - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - -IF (@v >= 13 - AND @QueryFilter = 'all' - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) - OR (LEFT(@QueryFilter, 3) = 'fun') -BEGIN - SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - - -/******************************************************************************* - * - * Because the trigger execution count in SQL Server 2008R2 and earlier is not - * correct, we ignore triggers for these versions of SQL Server. If you'd like - * to include trigger numbers, just know that the ExecutionCount, - * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for - * triggers on these versions of SQL Server. - * - * This is why we can't have nice things. - * - ******************************************************************************/ -IF (@UseTriggersAnyway = 1 OR @v >= 11) - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (@QueryFilter = 'all') - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) -BEGIN - RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; - - /* Trigger level information from the plan cache */ - SET @sql += @insert_list ; - - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; - - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - -DECLARE @sort NVARCHAR(MAX); - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - -SET @sql += N' -INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) -SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount -FROM (SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount, - ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn - FROM ##bou_BlitzCacheProcs) AS x -WHERE x.rn = 1 -OPTION (RECOMPILE); -'; - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' - WHEN N'reads' THEN N'TotalReads' - WHEN N'writes' THEN N'TotalWrites' - WHEN N'duration' THEN N'TotalDuration' - WHEN N'executions' THEN N'ExecutionCount' - WHEN N'compiles' THEN N'PlanCreationTime' - WHEN N'memory grant' THEN N'MaxGrantKB' - WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' - WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' - WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' - WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' - WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' - WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - -IF @Reanalyze = 0 -BEGIN - RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; - - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT', @Top, @DurationFilter_i, @MinutesBack; -END; - - -/* Update ##bou_BlitzCacheProcs to get Stored Proc info - * This should get totals for all statements in a Stored Proc - */ -RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; -;WITH agg AS ( - SELECT - b.SqlHandle, - SUM(b.MinReturnedRows) AS MinReturnedRows, - SUM(b.MaxReturnedRows) AS MaxReturnedRows, - SUM(b.AverageReturnedRows) AS AverageReturnedRows, - SUM(b.TotalReturnedRows) AS TotalReturnedRows, - SUM(b.LastReturnedRows) AS LastReturnedRows, - SUM(b.MinGrantKB) AS MinGrantKB, - SUM(b.MaxGrantKB) AS MaxGrantKB, - SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, - SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB - FROM ##bou_BlitzCacheProcs b - WHERE b.SPID = @@SPID - AND b.QueryHash IS NOT NULL - GROUP BY b.SqlHandle -) -UPDATE b - SET - b.MinReturnedRows = b2.MinReturnedRows, - b.MaxReturnedRows = b2.MaxReturnedRows, - b.AverageReturnedRows = b2.AverageReturnedRows, - b.TotalReturnedRows = b2.TotalReturnedRows, - b.LastReturnedRows = b2.LastReturnedRows, - b.MinGrantKB = b2.MinGrantKB, - b.MaxGrantKB = b2.MaxGrantKB, - b.MinUsedGrantKB = b2.MinUsedGrantKB, - b.MaxUsedGrantKB = b2.MaxUsedGrantKB -FROM ##bou_BlitzCacheProcs b -JOIN agg b2 -ON b2.SqlHandle = b.SqlHandle -WHERE b.QueryHash IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE) ; - -/* Compute the total CPU, etc across our active set of the plan cache. - * Yes, there's a flaw - this doesn't include anything outside of our @Top - * metric. - */ -RAISERROR('Computing CPU, duration, read, and write metrics', 0, 1) WITH NOWAIT; -DECLARE @total_duration BIGINT, - @total_cpu BIGINT, - @total_reads BIGINT, - @total_writes BIGINT, - @total_execution_count BIGINT; - -SELECT @total_cpu = SUM(TotalCPU), - @total_duration = SUM(TotalDuration), - @total_reads = SUM(TotalReads), - @total_writes = SUM(TotalWrites), - @total_execution_count = SUM(ExecutionCount) -FROM #p -OPTION (RECOMPILE) ; - -DECLARE @cr NVARCHAR(1) = NCHAR(13); -DECLARE @lf NVARCHAR(1) = NCHAR(10); -DECLARE @tab NVARCHAR(1) = NCHAR(9); - -/* Update CPU percentage for stored procedures */ -RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT PlanHandle, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE PlanHandle IS NOT NULL - AND SPID = @@SPID - GROUP BY PlanHandle, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.PlanHandle = y.PlanHandle - AND ##bou_BlitzCacheProcs.PlanHandle IS NOT NULL - AND ##bou_BlitzCacheProcs.SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET PercentCPU = y.PercentCPU, - PercentDuration = y.PercentDuration, - PercentReads = y.PercentReads, - PercentWrites = y.PercentWrites, - PercentExecutions = y.PercentExecutions, - ExecutionsPerMinute = y.ExecutionsPerMinute, - /* 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, ' ') -FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - CASE @total_cpu WHEN 0 THEN 0 - ELSE CAST((100. * TotalCPU) / @total_cpu AS MONEY) END AS PercentCPU, - CASE @total_duration WHEN 0 THEN 0 - ELSE CAST((100. * TotalDuration) / @total_duration AS MONEY) END AS PercentDuration, - CASE @total_reads WHEN 0 THEN 0 - ELSE CAST((100. * TotalReads) / @total_reads AS MONEY) END AS PercentReads, - CASE @total_writes WHEN 0 THEN 0 - ELSE CAST((100. * TotalWrites) / @total_writes AS MONEY) END AS PercentWrites, - CASE @total_execution_count WHEN 0 THEN 0 - ELSE CAST((100. * ExecutionCount) / @total_execution_count AS MONEY) END AS PercentExecutions, - CASE DATEDIFF(mi, PlanCreationTime, LastExecutionTime) - WHEN 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / DATEDIFF(mi, PlanCreationTime, LastExecutionTime)) AS MONEY) - END AS ExecutionsPerMinute - FROM ( - SELECT DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY DatabaseName, - SqlHandle, - QueryHash, - TotalCPU, - TotalDuration, - TotalReads, - TotalWrites, - ExecutionCount, - PlanCreationTime, - LastExecutionTime - ) AS x -) AS y -WHERE ##bou_BlitzCacheProcs.SqlHandle = y.SqlHandle - AND ##bou_BlitzCacheProcs.QueryHash = y.QueryHash - AND ##bou_BlitzCacheProcs.DatabaseName = y.DatabaseName - AND ##bou_BlitzCacheProcs.PlanHandle IS NULL -OPTION (RECOMPILE) ; - - - -/* Testing using XML nodes to speed up processing */ -RAISERROR(N'Begin XML nodes processing', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -INTO #statements -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements -SELECT QueryHash , - SqlHandle , - PlanHandle, - q.n.query('.') AS statement -FROM ##bou_BlitzCacheProcs p - CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) -WHERE p.SPID = @@SPID -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS query_plan -INTO #query_plan -FROM #statements p - CROSS APPLY p.statement.nodes('//p:QueryPlan') AS q(n) -OPTION (RECOMPILE) ; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -SELECT QueryHash , - SqlHandle , - q.n.query('.') AS relop -INTO #relop -FROM #query_plan p - CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) -OPTION (RECOMPILE) ; - - - --- high level plan stuff -RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans , - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END -FROM ( - SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; - --- statement level checks -RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET compile_timeout = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET compile_memory_limit_exceeded = 1 -FROM #statements s -JOIN ##bou_BlitzCacheProcs b -ON s.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -unparameterized_query AS ( - SELECT s.QueryHash, - unparameterized_query = CASE WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 1 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList/p:ColumnReference') = 0 THEN 1 - WHEN statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/p:QueryPlan/p:ParameterList') = 0 AND - statement.exist('//p:StmtSimple[@StatementOptmLevel[.="FULL"]]/*/p:RelOp/descendant::p:ScalarOperator/p:Identifier/p:ColumnReference[contains(@Column, "@")]') = 1 THEN 1 - END - FROM #statements AS s - ) -UPDATE b -SET b.unparameterized_query = u.unparameterized_query -FROM ##bou_BlitzCacheProcs b -JOIN unparameterized_query u -ON u.QueryHash = b.QueryHash -AND SPID = @@SPID -WHERE u.unparameterized_query = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -index_dml AS ( - SELECT s.QueryHash, - index_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 - END - FROM #statements s - ) - UPDATE b - SET b.index_dml = i.index_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN index_dml i - ON i.QueryHash = b.QueryHash - WHERE i.index_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); - -RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -table_dml AS ( - SELECT s.QueryHash, - table_dml = CASE WHEN statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 - WHEN statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 - END - FROM #statements AS s - ) - UPDATE b - SET b.table_dml = t.table_dml - FROM ##bou_BlitzCacheProcs AS b - JOIN table_dml t - ON t.QueryHash = b.QueryHash - WHERE t.table_dml = 1 - AND b.SPID = @@SPID - OPTION (RECOMPILE); - - -RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT INTO #est_rows -SELECT DISTINCT - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows -FROM #statements AS s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) -WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; - - UPDATE b - SET b.estimated_rows = er.estimated_rows - FROM ##bou_BlitzCacheProcs AS b - JOIN #est_rows er - ON er.QueryHash = b.QueryHash - WHERE b.SPID = @@SPID - AND b.QueryType = 'Statement' - OPTION (RECOMPILE); - - ---Gather costs -RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #plan_cost -SELECT DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, - s.SqlHandle, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash -FROM #statements s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS q(n) -WHERE statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 -OPTION (RECOMPILE); - -RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; -WITH pc AS ( - SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash - FROM #plan_cost AS pc - GROUP BY pc.QueryHash, pc.QueryPlanHash -) - UPDATE b - SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) - FROM pc - JOIN ##bou_BlitzCacheProcs b - ON b.QueryPlanHash = pc.QueryPlanHash - OR b.QueryHash = pc.QueryHash - WHERE b.QueryType NOT LIKE '%Procedure%' - OPTION (RECOMPILE); - -IF EXISTS ( -SELECT 1 -FROM ##bou_BlitzCacheProcs AS b -WHERE b.QueryType LIKE 'Procedure%' -) - -BEGIN - -RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, QueryCost AS ( - SELECT - DISTINCT - statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') AS SubTreeCost, - s.PlanHandle, - s.SqlHandle - FROM #statements AS s - WHERE PlanHandle IS NOT NULL -) -, QueryCostUpdate AS ( - SELECT - SUM(qc.SubTreeCost) OVER (PARTITION BY SqlHandle, PlanHandle) PlanTotalQuery, - qc.PlanHandle, - qc.SqlHandle - FROM QueryCost qc -) -INSERT INTO #proc_costs -SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle -FROM QueryCostUpdate AS qcu; - - -UPDATE b - SET b.QueryPlanCost = ca.PlanTotalQuery -FROM ##bou_BlitzCacheProcs AS b -CROSS APPLY ( - SELECT TOP 1 PlanTotalQuery - FROM #proc_costs qcu - WHERE qcu.PlanHandle = b.PlanHandle - ORDER BY PlanTotalQuery DESC -) ca -WHERE b.QueryType LIKE 'Procedure%' -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -END; - -UPDATE b -SET b.QueryPlanCost = 0.0 -FROM ##bou_BlitzCacheProcs b -WHERE b.QueryPlanCost IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET plan_warnings = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET implicit_conversions = 1 -FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle -AND SPID = @@SPID -AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 -OPTION (RECOMPILE); - --- operator level checks -RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , - relop.value('sum(/p:RelOp/@EstimateRewinds)', 'float') + relop.value('sum(/p:RelOp/@EstimateRebinds)', 'float') + 1.0 AS estimated_executions - FROM #relop qs - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE p -SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END -FROM ##bou_BlitzCacheProcs p - JOIN ( - SELECT r.SqlHandle, - 1 AS tvf_join - FROM #relop AS r - WHERE r.relop.exist('//p:RelOp[(@LogicalOp[.="Table-valued function"])]') = 1 - AND r.relop.exist('//p:RelOp[contains(@LogicalOp, "Join")]') = 1 - ) AS x ON p.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, - c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , - c.n.exist('//p:Warnings') AS relop_warnings -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) -) -UPDATE p -SET p.warning_no_join_predicate = x.warning_no_join_predicate, - p.no_stats_warning = x.no_stats_warning, - p.relop_warnings = x.relop_warnings -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.SqlHandle, - c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char -FROM #relop r -CROSS APPLY r.relop.nodes('//p:Object') AS c(n) -) -UPDATE p -SET is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -) -UPDATE p -SET p.function_count = x.function_count, - p.clr_function_count = x.clr_function_count -FROM ##bou_BlitzCacheProcs AS p -JOIN x ON x.SqlHandle = p.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET key_lookup_cost = x.key_lookup_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET remote_query_cost = x.remote_query_cost -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET sort_cost = (x.sort_io + x.sort_cpu) -FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM ##bou_BlitzCacheProcs b -JOIN #statements AS qs -ON b.SqlHandle = qs.SqlHandle -CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET -b.is_table_scan = x.is_table_scan, -b.backwards_scan = x.backwards_scan, -b.forced_index = x.forced_index, -b.forced_seek = x.forced_seek, -b.forced_scan = x.forced_scan -FROM ##bou_BlitzCacheProcs b -JOIN ( -SELECT - qs.SqlHandle, - 0 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:IndexScan') AS q(n) -UNION ALL -SELECT - qs.SqlHandle, - 1 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop qs -CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) -) AS x ON b.SqlHandle = x.SqlHandle -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_scalar = x.computed_column_function -FROM ( -SELECT qs.SqlHandle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_computed_filter = x.filter_function -FROM ( -SELECT -r.SqlHandle, -c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) -) x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -IndexOps AS -( - SELECT - r.QueryHash, - c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, - c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, - c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, - c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, - c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, - c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, - c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, - c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, - c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, - c.n.exist('@PhysicalOp[.="Table Delete"]') AS td - FROM #relop AS r - CROSS APPLY r.relop.nodes('/p:RelOp') c(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) -), iops AS -( - SELECT ios.QueryHash, - SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, - SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, - SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, - SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, - SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, - SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, - SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, - SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, - SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count - FROM IndexOps AS ios - WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', - 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', - 'Table Insert', 'Table Delete', 'Table Update') - GROUP BY ios.QueryHash) -UPDATE b -SET b.index_insert_count = iops.index_insert_count, - b.index_update_count = iops.index_update_count, - b.index_delete_count = iops.index_delete_count, - b.cx_insert_count = iops.cx_insert_count, - b.cx_update_count = iops.cx_update_count, - b.cx_delete_count = iops.cx_delete_count, - b.table_insert_count = iops.table_insert_count, - b.table_update_count = iops.table_update_count, - b.table_delete_count = iops.table_delete_count -FROM ##bou_BlitzCacheProcs AS b -JOIN iops ON iops.QueryHash = b.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET is_spatial = x.is_spatial -FROM ( -SELECT qs.SqlHandle, - 1 AS is_spatial -FROM #relop qs -CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) -WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle -AND SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( - 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -, selects -AS ( SELECT s.QueryHash - FROM #statements AS s - WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) -, spools -AS ( SELECT DISTINCT r.QueryHash, - c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, - c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, - c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds -FROM #relop AS r -JOIN selects AS s -ON s.QueryHash = r.QueryHash -CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) -WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 -) -UPDATE b - SET b.index_spool_rows = sp.estimated_rows, - b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM ##bou_BlitzCacheProcs b -JOIN spools sp -ON sp.QueryHash = b.QueryHash -OPTION ( RECOMPILE ); - - -/* 2012+ only */ -IF @v >= 11 -BEGIN - - RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET is_forced_serial = 1 - FROM #query_plan qp - WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle - AND SPID = @@SPID - AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 - AND (##bou_BlitzCacheProcs.is_parallel = 0 OR ##bou_BlitzCacheProcs.is_parallel IS NULL) - OPTION (RECOMPILE); - - - RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs - SET columnstore_row_mode = x.is_row_mode - FROM ( - SELECT - qs.SqlHandle, - relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode - FROM #relop qs - WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 - ) AS x - WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle - AND SPID = @@SPID - OPTION (RECOMPILE) ; - -END; - -/* 2014+ only */ -IF @v >= 12 -BEGIN - RAISERROR('Checking for downlevel cardinality estimators being used on SQL Server 2014.', 0, 1) WITH NOWAIT; - - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; -END ; - -/* 2016+ only */ -IF @v >= 13 -BEGIN - RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; - - WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE p - SET p.is_row_level = 1 - FROM ##bou_BlitzCacheProcs p - JOIN #statements s ON p.QueryHash = s.QueryHash - WHERE SPID = @@SPID - AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 - OPTION (RECOMPILE) ; -END ; - -/* 2017+ only */ -IF @v >= 14 -BEGIN - -RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #stats_agg -SELECT qp.SqlHandle, - x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, - x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] -FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c); - -RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; -WITH stale_stats AS ( - SELECT sa.SqlHandle - FROM #stats_agg AS sa - GROUP BY sa.SqlHandle - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000 -) -UPDATE b -SET stale_stats = 1 -FROM ##bou_BlitzCacheProcs b -JOIN stale_stats os -ON b.SqlHandle = os.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -aj AS ( - SELECT - SqlHandle - FROM #relop AS r - CROSS APPLY r.relop.nodes('//p:RelOp') x(c) - WHERE x.c.exist('@IsAdaptive[.=1]') = 1 -) -UPDATE b -SET b.is_adaptive = 1 -FROM ##bou_BlitzCacheProcs b -JOIN aj -ON b.SqlHandle = aj.SqlHandle -AND b.SPID = @@SPID -OPTION (RECOMPILE) ; - -END; - --- query level checks -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') , - SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , - SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), - CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , - CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , - CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , - CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float') -FROM #query_plan qp -WHERE qp.QueryHash = ##bou_BlitzCacheProcs.QueryHash -AND SPID = @@SPID -OPTION (RECOMPILE); - - -/* END Testing using XML nodes to speed up processing */ -RAISERROR(N'Gathering additional plan level information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans, - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END , - is_trivial = CASE WHEN QueryPlan.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 THEN 1 END -FROM ( -SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash -FROM ##bou_BlitzCacheProcs -WHERE SPID = @@SPID -GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; - -/* Update to grab stored procedure name for individual statements */ -RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; -UPDATE p -SET QueryType = QueryType + ' (parent ' + - + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) - + '.' - + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' -FROM ##bou_BlitzCacheProcs p - JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle -WHERE QueryType = 'Statement' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - -/* Trace Flag Checks 2014 SP2 and 2016 SP1 only)*/ -IF @v >= 11 -BEGIN -RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, tf_pretty AS ( -SELECT qp.QueryHash, - qp.SqlHandle, - q.n.value('@Value', 'INT') AS trace_flag, - q.n.value('@Scope', 'VARCHAR(10)') AS scope -FROM #query_plan qp -CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) -) -INSERT INTO #trace_flags -SELECT DISTINCT tf1.SqlHandle , tf1.QueryHash, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Global' - FOR XML PATH(N'')), 1, 2, N'' - ) AS global_trace_flags, - STUFF(( - SELECT DISTINCT ', ' + CONVERT(VARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.SqlHandle = tf2.SqlHandle - AND tf1.QueryHash = tf2.QueryHash - AND tf2.scope = 'Session' - FOR XML PATH(N'')), 1, 2, N'' - ) AS session_trace_flags -FROM tf_pretty AS tf1 -OPTION (RECOMPILE); - -UPDATE p -SET p.trace_flags_session = tf.session_trace_flags -FROM ##bou_BlitzCacheProcs p -JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash -WHERE SPID = @@SPID -OPTION(RECOMPILE); -END; - - -RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -is_paul_white_electric AS ( -SELECT 1 AS [is_paul_white_electric], -r.SqlHandle -FROM #relop AS r -CROSS APPLY r.relop.nodes('//p:RelOp') c(n) -WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 -) -UPDATE b -SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM ##bou_BlitzCacheProcs AS b -JOIN is_paul_white_electric ipwe -ON ipwe.SqlHandle = b.SqlHandle -WHERE b.SPID = @@SPID -OPTION (RECOMPILE); - -IF EXISTS ( SELECT 1 - FROM ##bou_BlitzCacheProcs AS bbcp - WHERE bbcp.implicit_conversions = 1 - OR bbcp.QueryType LIKE '%Procedure or Function: %') -BEGIN - -RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; - -RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #variable_info ( SPID, QueryHash, SqlHandle, proc_name, variable_name, variable_datatype, compile_time_value ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - q.n.value('@Column', 'NVARCHAR(128)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(128)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(1000)') AS compile_time_value -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) -WHERE b.SPID = @@SPID -OPTION ( RECOMPILE ); - -RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #conversion_info ( SPID, QueryHash, SqlHandle, proc_name, expression ) -SELECT DISTINCT @@SPID, - qp.QueryHash, - qp.SqlHandle, - CASE WHEN b.QueryType = 'Statement' THEN b.QueryType - ELSE SUBSTRING(b.QueryType, CHARINDEX('[', b.QueryType), LEN(b.QueryType) - CHARINDEX('[', b.QueryType)) - END AS proc_name, - qq.c.value('@Expression', 'NVARCHAR(128)') AS expression -FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b -ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) -OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) -CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) -WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 - AND qp.QueryHash IS NOT NULL - AND b.implicit_conversions = 1 -AND b.SPID = @@SPID -OPTION ( RECOMPILE ); - -RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; -INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) -SELECT @@SPID AS SPID, - ci.SqlHandle, - ci.QueryHash, - ci.proc_name, - CASE WHEN ci.at_charindex > 0 - AND ci.bracket_charindex > 0 - THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) - ELSE N'**no_variable**' - END AS variable_name, - N'**no_variable**' AS variable_datatype, - CASE WHEN ci.at_charindex = 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column**' - END AS converted_column_name, - CASE WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex = 0 - THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) - WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex > 0 - THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) - WHEN ci.at_charindex > 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column **' - END AS column_name, - CASE WHEN ci.paren_charindex > 0 - 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 - 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 -FROM #conversion_info AS ci -OPTION ( RECOMPILE ); - -IF EXISTS ( SELECT * - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name ) - BEGIN - RAISERROR(N'Updating variables', 0, 1) WITH NOWAIT; - UPDATE sp - SET sp.variable_datatype = vi.variable_datatype, - sp.compile_time_value = vi.compile_time_value - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) - OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) - AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); - END - ELSE - BEGIN - RAISERROR(N'Inserting variables', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, variable_name, variable_datatype, compile_time_value, proc_name ) - SELECT vi.SPID, vi.SqlHandle, vi.QueryHash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, vi.proc_name - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - END - -RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; -UPDATE s -SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) - ELSE s.variable_datatype - END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) - ELSE s.converted_to - END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) - WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' THEN - QUOTENAME(compile_time_value, '''') - ELSE s.compile_time_value - END -FROM #stored_proc_info AS s -OPTION(RECOMPILE); - -RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, - CONVERT(XML, - N' 'Statement' - THEN N'The stored procedure ' + spi.proc_name - ELSE N'This ad hoc statement' - END - + N' had the following implicit conversions: ' - + CHAR(10) - + STUFF(( - SELECT DISTINCT - @nl - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN N'The variable ' - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'The compiled value ' - WHEN spi2.column_name LIKE '%Expr%' - THEN 'The expression ' - ELSE N'The column ' - END - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN spi2.variable_name - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN spi2.compile_time_value - - ELSE spi2.column_name - END - + N' has a data type of ' - + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to - ELSE spi2.variable_datatype - END - + N' which caused implicit conversion on the column ' - + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' - THEN spi2.converted_column_name - WHEN spi2.column_name = N'**no_column**' - THEN spi2.converted_column_name - WHEN spi2.converted_column_name = N'**no_column**' - THEN spi2.column_name - WHEN spi2.column_name <> spi2.converted_column_name - THEN spi2.converted_column_name - ELSE spi2.column_name - END - + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'' - WHEN spi2.column_name LIKE '%Expr%' - THEN N'' - WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') - THEN ' with the value ' + RTRIM(spi2.compile_time_value) - ELSE N'' - END - + '.' - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) -UPDATE b -SET b.implicit_conversion_info = pk.implicit_conversion_info -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); - -RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; -WITH precheck AS ( -SELECT spi.SPID, - spi.SqlHandle, - spi.proc_name, -CONVERT(XML, - N' N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' - THEN spi2.variable_name + N' = ' - ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' - END - + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' - THEN @nl + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' - WHEN spi2.compile_time_value = N'NULL' - THEN spi2.compile_time_value - ELSE RTRIM(spi2.compile_time_value) - END - FROM #stored_proc_info AS spi2 - WHERE spi.SqlHandle = spi2.SqlHandle - AND spi2.proc_name <> N'Statement' - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @nl - + N' -- ?>' - ) AS cached_execution_parameters -FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name -) -UPDATE b -SET b.cached_execution_parameters = pk.cached_execution_parameters -FROM ##bou_BlitzCacheProcs AS b -JOIN precheck pk -ON pk.SqlHandle = b.SqlHandle -AND pk.SPID = b.SPID -OPTION(RECOMPILE); - - -END; --End implicit conversion information gathering - -UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN '' ELSE b.implicit_conversion_info END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN '' ELSE b.cached_execution_parameters END -FROM ##bou_BlitzCacheProcs AS b -WHERE b.SPID = @@SPID -OPTION(RECOMPILE); - -/*Begin Missing Index*/ - -IF EXISTS - (SELECT 1 FROM ##bou_BlitzCacheProcs AS bbcp WHERE bbcp.missing_index_count > 0 AND bbcp.SPID = @@SPID) - BEGIN - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_xml - SELECT qp.QueryHash, - qp.SqlHandle, - c.mg.value('@Impact', 'FLOAT') AS Impact, - c.mg.query('.') AS cmg - FROM #query_plan AS qp - CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) - WHERE qp.QueryHash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_schema - SELECT mix.QueryHash, mix.SqlHandle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , - c.mi.query('.') - FROM #missing_index_xml AS mix - CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_usage - SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, - c.cg.value('@Usage', 'NVARCHAR(128)'), - c.cg.query('.') - FROM #missing_index_schema ms - CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); - - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_detail - SELECT miu.QueryHash, - miu.SqlHandle, - miu.impact, - miu.database_name, - miu.schema_name, - miu.table_name, - miu.usage, - c.c.value('@Name', 'NVARCHAR(128)') - FROM #missing_index_usage AS miu - CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); - - INSERT #missing_index_pretty - SELECT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'EQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INEQUALITY' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INCLUDE' - AND m.QueryHash = m2.QueryHash - AND m.SqlHandle = m2.SqlHandle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] - FROM #missing_index_detail AS m - GROUP BY m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); - - WITH missing AS ( - SELECT mip.QueryHash, - mip.SqlHandle, - CONVERT(XML, - N'' - ) AS full_details - FROM #missing_index_pretty AS mip - GROUP BY mip.QueryHash, mip.SqlHandle, mip.impact - ) - UPDATE bbcp - SET bbcp.missing_indexes = m.full_details - FROM ##bou_BlitzCacheProcs AS bbcp - JOIN missing AS m - ON m.SqlHandle = bbcp.SqlHandle - AND SPID = @@SPID - OPTION(RECOMPILE); - - - END - - UPDATE b - SET b.missing_indexes = - CASE WHEN b.missing_indexes IS NULL - THEN '' - ELSE b.missing_indexes - END - FROM ##bou_BlitzCacheProcs AS b - WHERE b.SPID = @@SPID - OPTION(RECOMPILE); - -/*End Missing Index*/ - - - -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; - GOTO Results ; - END; - - -/* Set configuration values */ -RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; -DECLARE @execution_threshold INT = 1000 , - @parameter_sniffing_warning_pct TINYINT = 30, - /* This is in average reads */ - @parameter_sniffing_io_threshold BIGINT = 100000 , - @ctp_threshold_pct TINYINT = 10, - @long_running_query_warning_seconds BIGINT = 300 * 1000 , - @memory_grant_warning_percent INT = 10; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'frequent execution threshold' = LOWER(parameter_name)) -BEGIN - SELECT @execution_threshold = CAST(value AS INT) - FROM #configuration - WHERE 'frequent execution threshold' = LOWER(parameter_name) ; - - SET @msg = ' Setting "frequent execution threshold" to ' + CAST(@execution_threshold AS VARCHAR(10)) ; - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing variance percent' = LOWER(parameter_name)) -BEGIN - SELECT @parameter_sniffing_warning_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'parameter sniffing variance percent' = LOWER(parameter_name) ; - - SET @msg = ' Setting "parameter sniffing variance percent" to ' + CAST(@parameter_sniffing_warning_pct AS VARCHAR(3)) ; - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'parameter sniffing io threshold' = LOWER(parameter_name)) -BEGIN - SELECT @parameter_sniffing_io_threshold = CAST(value AS BIGINT) - FROM #configuration - WHERE 'parameter sniffing io threshold' = LOWER(parameter_name) ; - - SET @msg = ' Setting "parameter sniffing io threshold" to ' + CAST(@parameter_sniffing_io_threshold AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name)) -BEGIN - SELECT @ctp_threshold_pct = CAST(value AS TINYINT) - FROM #configuration - WHERE 'cost threshold for parallelism warning' = LOWER(parameter_name) ; - - SET @msg = ' Setting "cost threshold for parallelism warning" to ' + CAST(@ctp_threshold_pct AS VARCHAR(3)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'long running query warning (seconds)' = LOWER(parameter_name)) -BEGIN - SELECT @long_running_query_warning_seconds = CAST(value * 1000 AS BIGINT) - FROM #configuration - WHERE 'long running query warning (seconds)' = LOWER(parameter_name) ; - - SET @msg = ' Setting "long running query warning (seconds)" to ' + CAST(@long_running_query_warning_seconds AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -IF EXISTS (SELECT 1/0 FROM #configuration WHERE 'unused memory grant' = LOWER(parameter_name)) -BEGIN - SELECT @memory_grant_warning_percent = CAST(value AS INT) - FROM #configuration - WHERE 'unused memory grant' = LOWER(parameter_name) ; - - SET @msg = ' Setting "unused memory grant" to ' + CAST(@memory_grant_warning_percent AS VARCHAR(10)); - - RAISERROR(@msg, 0, 1) WITH NOWAIT; -END; - -DECLARE @ctp INT ; - -SELECT @ctp = NULLIF(CAST(value AS INT), 0) -FROM sys.configurations -WHERE name = 'cost threshold for parallelism' -OPTION (RECOMPILE); - - -/* Update to populate checks columns */ -RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , - parameter_sniffing = CASE WHEN AverageReads > @parameter_sniffing_io_threshold - AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold - AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , - near_parallel = CASE WHEN QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, - long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 - WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 - WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, - is_key_lookup_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, - is_sort_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, - is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, - is_forced_serial = CASE WHEN is_forced_serial = 1 THEN 1 END, - is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, - long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 THEN 1 END, - low_cost_high_cpu = CASE WHEN QueryPlanCost < @ctp AND AverageCPU > 500. AND QueryPlanCost * 10 < AverageCPU THEN 1 END, - is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, - is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, - is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 10000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 10000) THEN 1 END -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - - -RAISERROR('Checking for forced parameterization and cursors.', 0, 1) WITH NOWAIT; - -/* Set options checks */ -UPDATE p - SET is_forced_parameterized = CASE WHEN (CAST(pa.value AS INT) & 131072 = 131072) THEN 1 END , - is_forced_plan = CASE WHEN (CAST(pa.value AS INT) & 4 = 4) THEN 1 END , - SetOptions = SUBSTRING( - CASE WHEN (CAST(pa.value AS INT) & 1 = 1) THEN ', ANSI_PADDING' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8 = 8) THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 16 = 16) THEN ', ANSI_WARNINGS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 32 = 32) THEN ', ANSI_NULLS' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 64 = 64) THEN ', QUOTED_IDENTIFIER' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + - CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute = 'set_options' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - -/* Cursor checks */ -UPDATE p -SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END -FROM ##bou_BlitzCacheProcs p - CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa -WHERE pa.attribute LIKE '%cursor%' -AND SPID = @@SPID -OPTION (RECOMPILE) ; - - - -RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; -/* Populate warnings */ -UPDATE ##bou_BlitzCacheProcs -SET Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -WHERE SPID = @@SPID -OPTION (RECOMPILE) ; - - -RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; -WITH statement_warnings AS - ( -SELECT DISTINCT - SqlHandle, - Warnings = SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN busy_loops = 1 THEN ', Busy Loops' ELSE '' END + - CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + - CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' CLR function(s)' ELSE '' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + - CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs b -WHERE SPID = @@SPID -AND QueryType LIKE 'Statement (parent%' - ) -UPDATE b -SET b.Warnings = s.Warnings -FROM ##bou_BlitzCacheProcs AS b -JOIN statement_warnings s -ON b.SqlHandle = s.SqlHandle -WHERE QueryType LIKE 'Procedure or Function%' -AND SPID = @@SPID -OPTION(RECOMPILE); - -RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; -WITH plan_handle AS ( -SELECT b.PlanHandle -FROM ##bou_BlitzCacheProcs b - CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp - CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp - WHERE tqp.encrypted = 0 - AND b.SPID = @@SPID - AND (qp.query_plan IS NULL - AND tqp.query_plan IS NOT NULL) -) -UPDATE b -SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' - , 'We couldn''t find a plan for this query. Possible reasons for this include dynamic SQL, RECOMPILE hints, and encrypted code.') -FROM ##bou_BlitzCacheProcs b -LEFT JOIN plan_handle ph ON -b.PlanHandle = ph.PlanHandle -WHERE b.QueryPlan IS NULL -AND b.SPID = @@SPID -OPTION (RECOMPILE); - -RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET Warnings = 'No warnings detected.' -WHERE Warnings = '' OR Warnings IS NULL -AND SPID = @@SPID -OPTION (RECOMPILE); - - -Results: -IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL -BEGIN - RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; - - /* send results to a table */ - DECLARE @insert_sql NVARCHAR(MAX) = N'' ; - - SET @insert_sql = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + N'(ID bigint NOT NULL IDENTITY(1,1), - ServerName nvarchar(256), - CheckDate DATETIMEOFFSET, - Version nvarchar(256), - QueryType nvarchar(256), - Warnings varchar(max), - DatabaseName sysname, - SerialDesiredMemory float, - SerialRequiredMemory float, - AverageCPU bigint, - TotalCPU bigint, - PercentCPUByType money, - CPUWeight money, - AverageDuration bigint, - TotalDuration bigint, - DurationWeight money, - PercentDurationByType money, - AverageReads bigint, - TotalReads bigint, - ReadWeight money, - PercentReadsByType money, - AverageWrites bigint, - TotalWrites bigint, - WriteWeight money, - PercentWritesByType money, - ExecutionCount bigint, - ExecutionWeight money, - PercentExecutionsByType money,' + N' - ExecutionsPerMinute money, - PlanCreationTime datetime, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime datetime, - PlanHandle varbinary(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' - ELSE ''N/A'' END, - SqlHandle varbinary(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' - ELSE ''N/A'' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryHash binary(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryPlanHash binary(8), - StatementStartOffset int, - StatementEndOffset int, - MinReturnedRows bigint, - MaxReturnedRows bigint, - AverageReturnedRows money, - TotalReturnedRows bigint, - QueryText nvarchar(max), - QueryPlan xml, - NumberOfPlans int, - NumberOfDistinctPlans int, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryPlanCost FLOAT, - CONSTRAINT [PK_' +CAST(NEWID() AS NCHAR(36)) + '] PRIMARY KEY CLUSTERED(ID))'; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql ; - - IF @CheckDateOverride IS NULL - BEGIN - SET @CheckDateOverride = SYSDATETIMEOFFSET(); - END - - - SET @insert_sql =N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + N''') ' - + 'INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + N' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ) ' - + N'SELECT TOP (@Top) ' - + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), N'''') + N', @CheckDateOverride, ' - + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), N'''') + ', ' - + N' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ' - + N' FROM ##bou_BlitzCacheProcs ' - + N' WHERE 1=1 '; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @insert_sql += N' AND ExecutionCount >= @MinimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @insert_sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; - - SET @insert_sql += N' AND SPID = @@SPID '; - - SELECT @insert_sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; - - SET @insert_sql += N' OPTION (RECOMPILE) ; '; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; - - RETURN; -END; -ELSE IF @ExportToExcel = 1 -BEGIN - RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; - - /* excel output */ - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) - OPTION(RECOMPILE); - - SET @sql = N' - SELECT TOP (@Top) - DatabaseName AS [Database Name], - QueryPlanCost AS [Cost], - QueryText, - QueryType AS [Query Type], - Warnings, - ExecutionCount, - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - PercentExecutionsByType AS [% Executions (Type)], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - PercentCPUByType AS [% CPU (Type)], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - PercentDurationByType AS [% Duration (Type)], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - PercentReadsByType AS [% Reads (Type)], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows, - AverageReturnedRows, - MinReturnedRows, - MaxReturnedRows, - MinGrantKB, - MaxGrantKB, - MinUsedGrantKB, - MaxUsedGrantKB, - PercentMemoryGrantUsed, - AvgMaxMemoryGrant, - NumberOfPlans, - NumberOfDistinctPlans, - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - StatementStartOffset, - StatementEndOffset, - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - QueryHash, - QueryPlanHash, - COALESCE(SetOptions, '''') AS [SET Options] - FROM ##bou_BlitzCacheProcs - WHERE 1 = 1 - AND SPID = @@SPID ' + @nl; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; - - SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN ' TotalCPU ' - WHEN 'reads' THEN ' TotalReads ' - WHEN 'writes' THEN ' TotalWrites ' - WHEN 'duration' THEN ' TotalDuration ' - WHEN 'executions' THEN ' ExecutionCount ' - WHEN 'compiles' THEN ' PlanCreationTime ' - WHEN 'memory grant' THEN 'MaxGrantKB' - WHEN 'avg cpu' THEN 'AverageCPU' - WHEN 'avg reads' THEN 'AverageReads' - WHEN 'avg writes' THEN 'AverageWrites' - WHEN 'avg duration' THEN 'AverageDuration' - WHEN 'avg executions' THEN 'ExecutionsPerMinute' - WHEN 'avg memory grant' THEN 'AvgMaxMemoryGrant' - END + N' DESC '; - - SET @sql += N' OPTION (RECOMPILE) ; '; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @minimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; -END; - - -RAISERROR('Displaying analysis of plan cache.', 0, 1) WITH NOWAIT; - -DECLARE @columns NVARCHAR(MAX) = N'' ; - -IF @ExpertMode = 0 -BEGIN - RAISERROR(N'Returning ExpertMode = 0', 0, 1) WITH NOWAIT; - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], - ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Avg Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Avg Writes], - PercentWrites AS [Write Weight], - AverageReturnedRows AS [Average Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - COALESCE(SetOptions, '''') AS [SET Options] '; -END; -ELSE -BEGIN - SET @columns = N' DatabaseName AS [Database], - QueryPlanCost AS [Cost], - QueryText AS [Query Text], - QueryType AS [Query Type], - Warnings AS [Warnings], - QueryPlan AS [Query Plan], - missing_indexes AS [Missing Indexes], - implicit_conversion_info AS [Implicit Conversion Info], - cached_execution_parameters AS [Cached Execution Parameters], ' + @nl; - - IF @ExpertMode = 2 /* Opserver */ - BEGIN - RAISERROR(N'Returning Expert Mode = 2', 0, 1) WITH NOWAIT; - SET @columns += N' - SUBSTRING( - CASE WHEN warning_no_join_predicate = 1 THEN '', 20'' ELSE '''' END + - CASE WHEN compile_timeout = 1 THEN '', 18'' ELSE '''' END + - CASE WHEN compile_memory_limit_exceeded = 1 THEN '', 19'' ELSE '''' END + - CASE WHEN busy_loops = 1 THEN '', 16'' ELSE '''' END + - CASE WHEN is_forced_plan = 1 THEN '', 3'' ELSE '''' END + - CASE WHEN is_forced_parameterized > 0 THEN '', 5'' ELSE '''' END + - CASE WHEN unparameterized_query = 1 THEN '', 23'' ELSE '''' END + - CASE WHEN missing_index_count > 0 THEN '', 10'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_cursor = 1 THEN '', 4'' ELSE '''' END + - CASE WHEN is_parallel = 1 THEN '', 6'' ELSE '''' END + - CASE WHEN near_parallel = 1 THEN '', 7'' ELSE '''' END + - CASE WHEN frequent_execution = 1 THEN '', 1'' ELSE '''' END + - CASE WHEN plan_warnings = 1 THEN '', 8'' ELSE '''' END + - CASE WHEN parameter_sniffing = 1 THEN '', 2'' ELSE '''' END + - CASE WHEN long_running = 1 THEN '', 9'' ELSE '''' END + - CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + - CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + - CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + - CASE WHEN plan_multiple_plans = 1 THEN '', 21'' ELSE '''' END + - CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + - CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + - CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + - CASE WHEN is_key_lookup_expensive = 1 THEN '', 26'' ELSE '''' END + - CASE WHEN is_remote_query_expensive = 1 THEN '', 28'' ELSE '''' END + - CASE WHEN trace_flags_session IS NOT NULL THEN '', 29'' ELSE '''' END + - CASE WHEN is_unused_grant = 1 THEN '', 30'' ELSE '''' END + - CASE WHEN function_count > 0 THEN '', 31'' ELSE '''' END + - CASE WHEN clr_function_count > 0 THEN '', 32'' ELSE '''' END + - CASE WHEN PlanCreationTimeHours <= 4 THEN '', 33'' ELSE '''' END + - CASE WHEN is_table_variable = 1 THEN '', 34'' ELSE '''' END + - CASE WHEN no_stats_warning = 1 THEN '', 35'' ELSE '''' END + - CASE WHEN relop_warnings = 1 THEN '', 36'' ELSE '''' END + - CASE WHEN is_table_scan = 1 THEN '', 37'' ELSE '''' END + - CASE WHEN backwards_scan = 1 THEN '', 38'' ELSE '''' END + - CASE WHEN forced_index = 1 THEN '', 39'' ELSE '''' END + - CASE WHEN forced_seek = 1 OR forced_scan = 1 THEN '', 40'' ELSE '''' END + - CASE WHEN columnstore_row_mode = 1 THEN '', 41'' ELSE '''' END + - CASE WHEN is_computed_scalar = 1 THEN '', 42'' ELSE '''' END + - CASE WHEN is_sort_expensive = 1 THEN '', 43'' ELSE '''' END + - CASE WHEN is_computed_filter = 1 THEN '', 44'' ELSE '''' END + - CASE WHEN index_ops >= 5 THEN '', 45'' ELSE '''' END + - CASE WHEN is_row_level = 1 THEN '', 46'' ELSE '''' END + - CASE WHEN is_spatial = 1 THEN '', 47'' ELSE '''' END + - CASE WHEN index_dml = 1 THEN '', 48'' ELSE '''' END + - CASE WHEN table_dml = 1 THEN '', 49'' ELSE '''' END + - CASE WHEN long_running_low_cpu = 1 THEN '', 50'' ELSE '''' END + - CASE WHEN low_cost_high_cpu = 1 THEN '', 51'' ELSE '''' END + - CASE WHEN stale_stats = 1 THEN '', 52'' ELSE '''' END + - CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + - CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + - CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + - CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + - CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END - , 2, 200000) AS opserver_warning , ' + @nl ; - END; - - SET @columns += N' ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentExecutionsByType AS [% Executions (Type)], - PercentCPUByType AS [% CPU (Type)], - PercentDurationByType AS [% Duration (Type)], - PercentReadsByType AS [% Reads (Type)], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows AS [Total Rows], - AverageReturnedRows AS [Avg Rows], - MinReturnedRows AS [Min Rows], - MaxReturnedRows AS [Max Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - NumberOfPlans AS [# Plans], - NumberOfDistinctPlans AS [# Distinct Plans], - PlanCreationTime AS [Created At], - LastExecutionTime AS [Last Execution], - CachedPlanSize AS [Cached Plan Size (KB)], - CompileTime AS [Compile Time (ms)], - CompileCPU AS [Compile CPU (ms)], - CompileMemory AS [Compile memory (KB)], - COALESCE(SetOptions, '''') AS [SET Options], - PlanHandle AS [Plan Handle], - SqlHandle AS [SQL Handle], - [SQL Handle More Info], - QueryHash AS [Query Hash], - [Query Hash More Info], - QueryPlanHash AS [Query Plan Hash], - StatementStartOffset, - StatementEndOffset, - [Remove Plan Handle From Cache], - [Remove SQL Handle From Cache]'; -END; - - - -SET @sql = N' -SELECT TOP (@Top) ' + @columns + @nl + N' -FROM ##bou_BlitzCacheProcs -WHERE SPID = @spid ' + @nl; - -IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount ' + @nl; - END; - -IF @MinutesBack IS NOT NULL - BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; - END; - -SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; -SET @sql += N' OPTION (RECOMPILE) ; '; - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; - -EXEC sp_executesql @sql, N'@Top INT, @spid INT, @minimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; - -IF @HideSummary = 0 AND @ExportToExcel = 0 -BEGIN - IF @Reanalyze = 0 - BEGIN - RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; - - /* Build summary data */ - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE frequent_execution = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 1, - 100, - 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', - 'Queries are being executed more than ' - + CAST (@execution_threshold AS VARCHAR(5)) - + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE parameter_sniffing = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2, - 50, - 'Parameterization', - 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', - 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; - - /* Forced execution plans */ - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_plan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 3, - 5, - 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', - 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 4, - 200, - 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_forced_parameterized = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 5, - 50, - 'Parameterization', - 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', - 'Execution plans have been compiled with forced parameterization.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 6, - 200, - 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', - 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE near_parallel = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 7, - 200, - 'Execution Plans', - 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', - 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE plan_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 8, - 50, - 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE long_running = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 9, - 50, - 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', - 'Long running queries have been found. These are queries with an average duration longer than ' - + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) - + ' second(s). These queries should be investigated for additional tuning options.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.missing_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 10, - 50, - 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', - 'Queries found with missing indexes.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.downlevel_estimator = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 13, - 200, - 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', - 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE implicit_conversions = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 14, - 50, - 'Performance', - 'Implicit Conversions', - 'http://brentozar.com/go/implicit', - 'One or more queries are comparing two fields that are not of the same data type.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE busy_loops = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 16, - 10, - 'Performance', - 'Frequently executed operators', - 'http://brentozar.com/blitzcache/busy-loops/', - 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE tvf_join = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 17, - 50, - 'Performance', - 'Joining to table valued functions', - 'http://brentozar.com/blitzcache/tvf-join/', - 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_timeout = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 18, - 50, - 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', - 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE compile_memory_limit_exceeded = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 19, - 50, - 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', - 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE warning_no_join_predicate = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 20, - 10, - 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', - 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE plan_multiple_plans = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 21, - 200, - 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', - 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unmatched_index_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 22, - 100, - 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', - 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE unparameterized_query = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 23, - 100, - 'Parameterization', - 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', - 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE is_trivial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 24, - 100, - 'Execution Plans', - 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', - 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_forced_serial= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 25, - 10, - 'Execution Plans', - 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', - 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_key_lookup_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 26, - 100, - 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', - 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_remote_query_expensive= 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 28, - 100, - 'Execution Plans', - 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', - 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.trace_flags_session IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 29, - 100, - 'Trace Flags', - 'Session Level Trace Flags Enabled', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'Someone is enabling session level Trace Flags in a query.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_unused_grant IS NOT NULL - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 30, - 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', - 'https://www.brentozar.com/blitzcache/unused-memory-grants/', - 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 31, - 100, - 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.clr_function_count > 0 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 32, - 100, - 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; - - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_variable = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 33, - 100, - 'Table Variables detected', - 'Beware nasty side effects', - 'https://www.brentozar.com/blitzcache/table-variables/', - 'All modifications are single threaded, and selects have really low row estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.no_stats_warning = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 35, - 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', - 'https://www.brentozar.com/blitzcache/columns-no-statistics/', - 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.relop_warnings = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 36, - 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Check the plan for more details.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_table_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 37, - 100, - 'Table Scans', - 'Your database has HEAPs', - 'https://www.brentozar.com/archive/2012/05/video-heaps/', - 'This may not be a problem. Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.backwards_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 38, - 100, - 'Backwards Scans', - 'Indexes are being read backwards', - 'https://www.brentozar.com/blitzcache/backwards-scans/', - 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_index = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 39, - 100, - 'Index forcing', - 'Someone is using hints to force index usage', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans, and will prevent missing index requests.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.forced_seek = 1 - OR p.forced_scan = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 40, - 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.columnstore_row_mode = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 41, - 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', - 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', - 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_scalar = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 42, - 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', - 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', - 'This can cause a whole mess of bad serializartion problems.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_sort_expensive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 43, - 100, - 'Execution Plans', - 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', - 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_computed_filter = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 44, - 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Someone put a Scalar UDF in the WHERE clause!') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_ops >= 5 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 45, - 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', - 'No URL yet', - 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_row_level = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', - 'No URL yet', - 'You may see a lot of confusing junk in your query plan.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spatial = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 47, - 200, - 'Spatial Abuse', - 'You hit a Spatial Index', - 'No URL yet', - 'Purely informational.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.index_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 48, - 150, - 'Index DML', - 'Indexes were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.table_dml = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 49, - 150, - 'Table DML', - 'Tables were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.long_running_low_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 50, - 150, - 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', - 'No URL yet', - 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.low_cost_high_cpu = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 51, - 150, - 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', - 'No URL yet', - 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.stale_stats = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 52, - 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', - 'No URL yet', - 'Ever heard of updating statistics?') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_adaptive = 1 - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', - 'No URL yet', - 'Joe Sack rules.') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_expensive = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 54, - 150, - 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_spool_more_rows = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 55, - 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_bad_estimate = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 56, - 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', - 'No URL yet', - 'This may indicate a performance problem if mismatches occur regularly') ; - - IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p - WHERE p.is_paul_white_electric = 1 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 998, - 200, - 'Is Paul White Electric?', - 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - - IF @v >= 14 - BEGIN - - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT - @@SPID, - 999, - 200, - 'Database Level Statistics', - 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], - '' AS URL, - 'Consider updating statistics more frequently,' AS [Details] - FROM #stats_agg AS sa - GROUP BY sa.[Database] - HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.ModificationCount) >= 100000; - - - END; - - - IF EXISTS (SELECT 1/0 - FROM #plan_creation p - WHERE (p.percent_24 > 0) - AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT SPID, - 999, - 254, - 'Plan Cache Information', - 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) - + ' total plans in your cache, with ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) - + '% plans created in the past 24 hours, ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) - + '% created in the past 4 hours, and ' - + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) - + '% created in the past 1 hour.', - '', - 'If these percentages are high, it may be a sign of memory pressure or plan cache instability.' - FROM #plan_creation p ; - - IF @v >= 11 - BEGIN - IF EXISTS (SELECT 1/0 - FROM #trace_flags AS tf - WHERE tf.global_trace_flags IS NOT NULL - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 1000, - 255, - 'Global Trace Flags Enabled', - 'You have Global Trace Flags enabled on your server', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; - END; - - IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr - WHERE bcr.Priority = 2147483646 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2147483646, - 255, - 'Need more help?' , - 'Paste your plan on the internet!', - 'http://pastetheplan.com', - 'This makes it easy to share plans and post them to Q&A sites like https://dba.stackexchange.com/!') ; - - - - IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr - WHERE bcr.Priority = 2147483647 - ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (@@SPID, - 2147483647, - 255, - 'Thanks for using sp_BlitzCache!' , - 'From Your Community Volunteers', - 'http://FirstResponderKit.org', - 'We hope you found this tool useful. Current version: ' + @Version + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.') ; - - END; - - - SELECT Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - FROM ##bou_BlitzCacheResults - WHERE SPID = @@SPID - GROUP BY Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - ORDER BY Priority ASC, CheckID ASC - OPTION (RECOMPILE); -END; - -IF @Debug = 1 - BEGIN - - SELECT '##bou_BlitzCacheResults' AS table_name, * - FROM ##bou_BlitzCacheResults - OPTION ( RECOMPILE ); - - SELECT '##bou_BlitzCacheProcs' AS table_name, * - FROM ##bou_BlitzCacheProcs - OPTION ( RECOMPILE ); - - SELECT '#statements' AS table_name, * - FROM #statements AS s - OPTION (RECOMPILE); - - SELECT '#query_plan' AS table_name, * - FROM #query_plan AS qp - OPTION (RECOMPILE); - - SELECT '#relop' AS table_name, * - FROM #relop AS r - OPTION (RECOMPILE); - - SELECT '#only_query_hashes' AS table_name, * - FROM #only_query_hashes - OPTION ( RECOMPILE ); - - SELECT '#ignore_query_hashes' AS table_name, * - FROM #ignore_query_hashes - OPTION ( RECOMPILE ); - - SELECT '#only_sql_handles' AS table_name, * - FROM #only_sql_handles - OPTION ( RECOMPILE ); - - SELECT '#ignore_sql_handles' AS table_name, * - FROM #ignore_sql_handles - OPTION ( RECOMPILE ); - - SELECT '#p' AS table_name, * - FROM #p - OPTION ( RECOMPILE ); - - SELECT '#checkversion' AS table_name, * - FROM #checkversion - OPTION ( RECOMPILE ); - - SELECT '#configuration' AS table_name, * - FROM #configuration - OPTION ( RECOMPILE ); - - SELECT '#stored_proc_info' AS table_name, * - FROM #stored_proc_info - OPTION ( RECOMPILE ); - - SELECT '#conversion_info' AS table_name, * - FROM #conversion_info AS ci - OPTION ( RECOMPILE ); - - SELECT '#variable_info' AS table_name, * - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - - SELECT '#plan_creation' AS table_name, * - FROM #plan_creation - OPTION ( RECOMPILE ); - - SELECT '#plan_cost' AS table_name, * - FROM #plan_cost - OPTION ( RECOMPILE ); - - SELECT '#proc_costs' AS table_name, * - FROM #proc_costs - OPTION ( RECOMPILE ); - - SELECT '#stats_agg' AS table_name, * - FROM #stats_agg - OPTION ( RECOMPILE ); - - SELECT '#trace_flags' AS table_name, * - FROM #trace_flags - OPTION ( RECOMPILE ); - - END; - - -RETURN; --Avoid going into the AllSort GOTO - -/*Begin code to sort by all*/ -AllSorts: -RAISERROR('Beginning all sort loop', 0, 1) WITH NOWAIT; - - -IF ( - @Top > 10 - AND @BringThePain = 0 - ) - BEGIN - RAISERROR( - ' - You''ve chosen a value greater than 10 to sort the whole plan cache by. - That can take a long time and harm performance. - Please choose a number <= 10, or set @BringThePain = 1 to signify you understand this might be a bad idea. - ', 0, 1) WITH NOWAIT; - RETURN; - END; - - -IF OBJECT_ID('tempdb..#checkversion_allsort') IS NULL - BEGIN - CREATE TABLE #checkversion_allsort - ( - version NVARCHAR(128), - common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1), - major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), - minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), - build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), - revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) - ); - - INSERT INTO #checkversion_allsort - (version) - SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) - OPTION ( RECOMPILE ); - END; - - -SELECT @v = common_version, - @build = build -FROM #checkversion_allsort -OPTION ( RECOMPILE ); - -IF OBJECT_ID('tempdb.. #bou_allsort') IS NULL - BEGIN - CREATE TABLE #bou_allsort - ( - Id INT IDENTITY(1, 1), - DatabaseName VARCHAR(128), - Cost FLOAT, - QueryText NVARCHAR(MAX), - QueryType NVARCHAR(256), - Warnings VARCHAR(MAX), - QueryPlan XML, - missing_indexes XML, - implicit_conversion_info XML, - cached_execution_parameters XML, - ExecutionCount BIGINT, - ExecutionsPerMinute MONEY, - ExecutionWeight MONEY, - TotalCPU BIGINT, - AverageCPU BIGINT, - CPUWeight MONEY, - TotalDuration BIGINT, - AverageDuration BIGINT, - DurationWeight MONEY, - TotalReads BIGINT, - AverageReads BIGINT, - ReadWeight MONEY, - TotalWrites BIGINT, - AverageWrites BIGINT, - WriteWeight MONEY, - AverageReturnedRows MONEY, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - AvgMaxMemoryGrant MONEY, - PlanCreationTime DATETIME, - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - SqlHandle VARBINARY(64), - SetOptions VARCHAR(MAX), - Pattern NVARCHAR(20) - ); - END; - -DECLARE @AllSortSql NVARCHAR(MAX) = N''; -DECLARE @MemGrant BIT; -SELECT @MemGrant = CASE WHEN ( - ( @v < 11 ) - OR ( - @v = 11 - AND @build < 6020 - ) - OR ( - @v = 12 - AND @build < 5000 - ) - OR ( - @v = 13 - AND @build < 1601 - ) - ) THEN 0 - ELSE 1 - END; - - -IF LOWER(@SortOrder) = 'all' -BEGIN -RAISERROR('Beginning for ALL', 0, 1) WITH NOWAIT; -SET @AllSortSql += N' - DECLARE @ISH NVARCHAR(MAX) = N'''' - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - '; - - IF @MemGrant = 0 - BEGIN - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - - IF @MemGrant = 1 - BEGIN - SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - -END; - - -IF LOWER(@SortOrder) = 'all avg' -BEGIN -RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; -SET @AllSortSql += N' - DECLARE @ISH NVARCHAR(MAX) = N'''' - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); - - '; - - IF @MemGrant = 0 - BEGIN - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; - - IF @MemGrant = 1 - BEGIN - SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) - - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; - - UPDATE #bou_allsort SET Pattern = ''avg memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; - IF @ExportToExcel = 1 - BEGIN - SET @AllSortSql += N' UPDATE #bou_allsort - SET - QueryPlan = NULL, - implicit_conversion_info = NULL, - cached_execution_parameters = NULL, - missing_indexes = NULL - OPTION (RECOMPILE); - - UPDATE ##bou_BlitzCacheProcs - SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) - OPTION(RECOMPILE);'; - END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; - END; -END; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@AllSortSql, 0, 4000); - PRINT SUBSTRING(@AllSortSql, 4000, 8000); - PRINT SUBSTRING(@AllSortSql, 8000, 12000); - PRINT SUBSTRING(@AllSortSql, 12000, 16000); - PRINT SUBSTRING(@AllSortSql, 16000, 20000); - PRINT SUBSTRING(@AllSortSql, 20000, 24000); - PRINT SUBSTRING(@AllSortSql, 24000, 28000); - PRINT SUBSTRING(@AllSortSql, 28000, 32000); - PRINT SUBSTRING(@AllSortSql, 32000, 36000); - PRINT SUBSTRING(@AllSortSql, 36000, 40000); - END; - - EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT', @i_DatabaseName = @DatabaseName, @i_Top = @Top; - - -/*End of AllSort section*/ - -END; /*Final End*/ - -GO -IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;') -GO - - -ALTER PROCEDURE [dbo].[sp_BlitzFirst] - @LogMessage NVARCHAR(4000) = NULL , - @Help TINYINT = 0 , - @AsOf DATETIMEOFFSET = NULL , - @ExpertMode TINYINT = 0 , - @Seconds INT = 5 , - @OutputType VARCHAR(20) = 'TABLE' , - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @OutputTableNameFileStats NVARCHAR(256) = NULL , - @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , - @OutputTableNameWaitStats NVARCHAR(256) = NULL , - @OutputTableNameBlitzCache NVARCHAR(256) = NULL , - @OutputTableRetentionDays TINYINT = 7 , - @OutputXMLasNVARCHAR TINYINT = 0 , - @FilterPlansByDatabase VARCHAR(MAX) = NULL , - @CheckProcedureCache TINYINT = 0 , - @FileLatencyThresholdMS INT = 100 , - @SinceStartup TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0 , - @LogMessageCheckID INT = 38, - @LogMessagePriority TINYINT = 1, - @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', - @LogMessageFinding VARCHAR(200) = 'Logged from sp_BlitzFirst', - @LogMessageURL VARCHAR(200) = '', - @LogMessageCheckDate DATETIMEOFFSET = NULL, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT - WITH EXECUTE AS CALLER, RECOMPILE -AS -BEGIN -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - - -IF @Help = 1 PRINT ' -sp_BlitzFirst from http://FirstResponderKit.org - -This script gives you a prioritized list of why your SQL Server is slow right now. - -This is not an overall health check - for that, check out sp_Blitz. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. It - may work just fine on 2005, and if it does, hug your parents. Just don''t - file support issues if it breaks. - - If a temp table called #CustomPerfmonCounters exists for any other session, - but not our session, this stored proc will fail with an error saying the - temp table #CustomPerfmonCounters does not exist. - - @OutputServerName is not functional yet. - - If @OutputDatabaseName, SchemaName, TableName, etc are quoted with brackets, - the write to table may silently fail. Look, I never said I was good at this. - -Unknown limitations of this version: - - None. Like Zombo.com, the only limit is yourself. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - -MIT License - -Copyright (c) 2017 Brent Ozar Unlimited - -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. - -' - - -RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; -DECLARE @StringToExecute NVARCHAR(MAX), - @ParmDefinitions NVARCHAR(4000), - @Parm1 NVARCHAR(4000), - @OurSessionID INT, - @LineFeed NVARCHAR(10), - @StockWarningHeader NVARCHAR(500), - @StockWarningFooter NVARCHAR(100), - @StockDetailsHeader NVARCHAR(100), - @StockDetailsFooter NVARCHAR(100), - @StartSampleTime DATETIMEOFFSET, - @FinishSampleTime DATETIMEOFFSET, - @FinishSampleTimeWaitFor DATETIME, - @ServiceName sysname, - @OutputTableNameFileStats_View NVARCHAR(256), - @OutputTableNamePerfmonStats_View NVARCHAR(256), - @OutputTableNameWaitStats_View NVARCHAR(256), - @OutputTableNameWaitStats_Categories NVARCHAR(256), - @ObjectFullName NVARCHAR(2000), - @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', - @BlitzCacheMinutesBack INT, - @BlitzCacheSortOrder VARCHAR(50), - @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , - @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , - @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName ; - -/* Sanitize our inputs */ -SELECT - @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), - @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), - @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), - @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); - -SELECT - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName), - @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), - @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), - @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), - /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ - @LineFeed = CHAR(13) + CHAR(10), - @StartSampleTime = SYSDATETIMEOFFSET(), - @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), - @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()), - @OurSessionID = @@SPID; - -IF @LogMessage IS NOT NULL - BEGIN - - RAISERROR('Saving LogMessage to table',10,1) WITH NOWAIT; - - /* Try to set the output table parameters if they don't exist */ - IF @OutputSchemaName IS NULL AND @OutputTableName IS NULL AND @OutputDatabaseName IS NULL - BEGIN - SET @OutputSchemaName = N'[dbo]'; - SET @OutputTableName = N'[BlitzFirst]'; - - /* Look for the table in the current database */ - SELECT TOP 1 @OutputDatabaseName = QUOTENAME(TABLE_CATALOG) - FROM INFORMATION_SCHEMA.TABLES - WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'BlitzFirst'; - - IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') - SET @OutputDatabaseName = '[DBAtools]'; - - END - - IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL - OR NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; - RETURN; - END - IF @LogMessageCheckDate IS NULL - SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @LogMessageCheckDate, 121) + ''', @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)' - - EXECUTE sp_executesql @StringToExecute, - N'@LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', - @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; - - RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; - - RETURN; - END - -IF @SinceStartup = 1 - SELECT @Seconds = 0, @ExpertMode = 1; - -IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT @StartSampleTime = DATEADD(ms, AVG(-wait_time_ms), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.dm_os_wait_stats w - WHERE wait_type IN ('BROKER_TASK_STOP','DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', - 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT') -ELSE IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.databases - WHERE database_id = 2; -ELSE - SELECT @StartSampleTime = SYSDATETIMEOFFSET(), @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()); - -IF @OutputType = 'SCHEMA' -BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)' - -END -ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL -BEGIN - /* They want to look into the past. */ - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT CheckDate, [Priority], [FindingsGroup], [Finding], [URL], CAST([Details] AS [XML]) AS Details,' - + '[HowToStopIt], [CheckID], [StartTime], [LoginName], [NTUserName], [OriginalLoginName], [ProgramName], [HostName], [DatabaseID],' - + '[DatabaseName], [OpenTransactionCount], [QueryPlan], [QueryText] FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE CheckDate >= DATEADD(mi, -15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' AND CheckDate <= DATEADD(mi, 15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; - EXEC(@StringToExecute); - - -END /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ -ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ -BEGIN - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - - - RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; - - /* - We start by creating #BlitzFirstResults. It's a temp table that will store - the results from our checks. Throughout the rest of this stored procedure, - we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the - end, we return these results to the end user. - - #BlitzFirstResults has a CheckID field, but there's no Check table. As we do - checks, we insert data into this table, and we manually put in the CheckID. - We (Brent Ozar Unlimited) maintain a list of the checks by ID#. You can - download that from http://FirstResponderKit.org if you want to build - a tool that relies on the output of sp_BlitzFirst. - */ - - IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL - DROP TABLE #BlitzFirstResults; - CREATE TABLE #BlitzFirstResults - ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt NVARCHAR(MAX) NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - QueryStatsNowID INT NULL, - QueryStatsFirstID INT NULL, - PlanHandle VARBINARY(64) NULL, - DetailsInt INT NULL, - ); - - IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL - DROP TABLE #WaitStats; - CREATE TABLE #WaitStats (Pass TINYINT NOT NULL, wait_type NVARCHAR(60), wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT, SampleTime DATETIMEOFFSET); - - IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL - DROP TABLE #FileStats; - CREATE TABLE #FileStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - avg_stall_read_ms INT , - avg_stall_write_ms INT - ); - - IF OBJECT_ID('tempdb..#QueryStats') IS NOT NULL - DROP TABLE #QueryStats; - CREATE TABLE #QueryStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass INT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [sql_handle] VARBINARY(64), - statement_start_offset INT, - statement_end_offset INT, - plan_generation_num BIGINT, - plan_handle VARBINARY(64), - execution_count BIGINT, - total_worker_time BIGINT, - total_physical_reads BIGINT, - total_logical_writes BIGINT, - total_logical_reads BIGINT, - total_clr_time BIGINT, - total_elapsed_time BIGINT, - creation_time DATETIMEOFFSET, - query_hash BINARY(8), - query_plan_hash BINARY(8), - Points TINYINT - ); - - IF OBJECT_ID('tempdb..#PerfmonStats') IS NOT NULL - DROP TABLE #PerfmonStats; - CREATE TABLE #PerfmonStats ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - Pass TINYINT NOT NULL, - SampleTime DATETIMEOFFSET NOT NULL, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL - ); - - IF OBJECT_ID('tempdb..#PerfmonCounters') IS NOT NULL - DROP TABLE #PerfmonCounters; - CREATE TABLE #PerfmonCounters ( - ID INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL - ); - - IF OBJECT_ID('tempdb..#FilterPlansByDatabase') IS NOT NULL - DROP TABLE #FilterPlansByDatabase; - CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); - - IF OBJECT_ID('tempdb..##WaitCategories') IS NULL - BEGIN - /* We reuse this one by default rather than recreate it every time. */ - CREATE TABLE ##WaitCategories - ( - WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, - WaitCategory NVARCHAR(128) NOT NULL, - Ignorable BIT DEFAULT 0 - ); - END /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ - - IF 504 <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) - BEGIN - TRUNCATE TABLE ##WaitCategories; - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_NETWORK_IO','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BACKUPIO','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_CONNECTION_RECEIVE_TASK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_DISPATCHER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_ENDPOINT_STATE_MUTEX','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_EVENTHANDLER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_FORWARDER','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_INIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_MASTERSTART','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_RECEIVE_WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_REGISTERALLENDPOINTS','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SERVICE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_START','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SHUTDOWN','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_STOP','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TASK_SUBMIT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TO_FLUSH','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_OBJECT','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_TABLE','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMISSION_WORK','Service Broker',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_SEND','Mirroring',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_STATE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_TMDOWN_REQUEST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_WAITFOR_OUTCOME','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_ENLIST','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_PREPARE','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_RECOVERY','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TM','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCNEW_TRANSACTION_ENLISTMENT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTCPNTSYNC','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EE_PMOLOCK','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXCHANGE','Parallelism',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('EXTERNAL_SCRIPT_NETWORK_IOF','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_READ','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FCB_REPLICA_WRITE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_METADATA_MUTEX','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_PROPERTYLIST_CACHE','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_RESTART_CRAWL','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FULLTEXT GATHERER','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AG_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_CRITICAL_SECTION_ENTRY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_MANAGER_MUTEX','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_AR_UNLOAD_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_ARCONTROLLER_NOTIFICATIONS_SUBSCRIBER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_BULK_LOCK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_BACKUP_QUEUE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CLUSAPI_CALL','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_COMPRESSED_CACHE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_CONNECTIVITY_INFO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_VERSIONING_STATE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RECOVERY','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_RESTART','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DATABASE_WAIT_FOR_TRANSITION_TO_VERSIONING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_COMMAND','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_COMPLETION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DB_OP_START_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBR_SUBSCRIBER_FILTER_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_MANAGER','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_PREPROC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_GROUP_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGCAPTURE_WAIT','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_LOGPROGRESS_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_DEQUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_EXCLUSIVE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_STARTUP_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_NOTIFICATION_WORKER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_PARTNER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_READ_ALL_NETWORKS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_CONNECTION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_RECOVERY_WAIT_FOR_UNDO','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_REPLICAINFO_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_CANCELLATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_FILE_LIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_LIMIT_BACKUPS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_SYNC_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_TIMEOUT_TASK','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SEEDING_WAIT_FOR_COMPLETION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNC_COMMIT','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_SYNCHRONIZING_THROTTLE','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TDS_LISTENER_SYNC_PROCESSING','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_THROTTLE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TIMER_TASK','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_DBRLIST','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_FLOW_CONTROL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_TRANSPORT_SESSION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_POOL','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_WORK_QUEUE','Replication',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_XRF_STACK_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('INSTANCE_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_QUEUE_LIMIT','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('IO_RETRY','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_DT','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_EX','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_KP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_NL','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_SH','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LATCH_UP','Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LAZYWRITER_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_BU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IS_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_IX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_NL_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RIn_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RS_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_RX_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_M_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SCH_S_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIU_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_SIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_U_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_UIX_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_PMM_LOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_RESERVE_APPEND','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MEMORY_GRANT_UPDATE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MGR_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSQL_XACT_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('MSSEARCH','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('NET_WAITFOR_PACKET','Network IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ONDEMAND_TASK_QUEUE','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_DT','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_EX','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_KP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_NL','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_SH','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGEIOLATCH_UP','Buffer IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_DT','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_EX','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_KP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPVDIDEVICE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLUSAPI_CLUSTERRESOURCECONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COCREATEINSTANCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_COGETCLASSOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_CREATEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_DELETEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETCOMMANDTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETNEXTROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETRESULT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_GETROWSBYBOOKMARK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBFLUSH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBREADAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_LBWRITEAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_QUERYINTERFACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEACCESSOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASEROWS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RELEASESESSION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_RESTARTPOSITION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SEQSTRMREADANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETDATAFAILURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_SETPARAMETERPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSEEKANDWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSETSIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMSTAT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_COM_STRMUNLOCKREGION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CONSOLEWRITE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CREATEPARAM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DEBUG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSADDLINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKEXISTCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSLINKHEALTHCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVELINK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSREMOVEROOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTFOLDERCHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DFSROOTSHARECHECK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ABORTREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_BEGINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_COMMITREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_ENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_DTC_PREPAREREQUESTDONE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FILESIZEGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_ABORTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_COMMITTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSAOLEDB_STARTTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_FSRECOVER_UNCONDITIONALUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_GETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_EVENT_WAIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_HTTP_REQUEST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_LOCKMONITOR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_MSS_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ODBCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLE_UNINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTORCOMMITTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_ABORTTRAN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETDATASOURCE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETLITERALINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETPROPERTYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_GETSCHEMALOCK','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_JOINTRANSACTION','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_RELEASE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDB_SETPROPERTIES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OLEDBOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACCEPTSECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ACQUIRECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHENTICATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHORIZATIONOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZGETINFORMATIONFROMCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZECONTEXTFROMSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_AUTHZINITIALIZERESOURCEMANAGER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_BACKUPREAD','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLOSEHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CLUSTEROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COMPLETEAUTHTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_COPYFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CREATEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTACQUIRECONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTIMPORTKEY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_CRYPTOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DECRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DELETESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEIOCONTROL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DIRSVC_NETWORKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DISCONNECTNAMEDPIPE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DOMAINSERVICESOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DSGETDCNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_DTCOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_ENCRYPTMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FILEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FINDFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FORMATMESSAGE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREECREDENTIALSHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_FREELIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GENERICOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETADDRINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETCOMPRESSEDFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETDISKFREESPACE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILEATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFILESIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETFINALFILEPATHBYHANDLE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETLONGPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETPROCADDRESS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMENAMEFORVOLUMEMOUNTPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_GETVOLUMEPATHNAME','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_INITIALIZESECURITYCONTEXT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LIBRARYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOADLIBRARY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOGONUSER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_LOOKUPACCOUNTSID','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MESSAGEQUEUEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_MOVEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETGROUPGETUSERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETLOCALGROUPGETMEMBERS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERGETLOCALGROUPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETUSERMODALSGET','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_NETVALIDATEPASSWORDPOLICYFREE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_OPENDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PDH_WMI_INIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PIPEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_PROCESSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYCONTEXTATTRIBUTES','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYREGISTRY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_QUERYSECURITYCONTEXTTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REMOVEDIRECTORY','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REPORTEVENT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_REVERTTOSELF','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_RSFXDEVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SECURITYOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SERVICEOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETENDOFFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEPOINTER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETFILEVALIDDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SETNAMEDSECURITYINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQLCLROPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_SQMLAUNCH','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYSIGNATURE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VERIFYTRUST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_VSSOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WAITFORSINGLEOBJECT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WINSOCKOPS','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WRITEFILEGATHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_OS_WSASETLASTERROR','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_REENLIST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_RESIZELOG','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDREDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ROLLFORWARDUNDO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SB_STOPENDPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SERVER_STARTUP','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SETRMINFO','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SHAREDMEM_GETDATA','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SNIOPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSHOST','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SOSTESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS','Preemptive',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STARTRM','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_CHECKPOINT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STREAMFCB_RECOVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_STRESSDRIVER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_TRANSIMPORT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_UNMARSHALPROPAGATIONTOKEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_VSS_CREATEVOLUMESNAPSHOT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETFINALIZE','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TARGETINIT','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_TIMERRUN','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XETESTING','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ACTION_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CHANGE_NOTIFIER_TERMINATION_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_CLUSTER_INTEGRATION','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_FAILOVER_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_JOIN','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_OFFLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_POST_ONLINE_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_SERVER_READY_CONNECTIONS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADR_WORKITEM_COMPLETED','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_HADRSIM','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC','Full Text Search',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_ASYNC_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QDS_SHUTDOWN_QUEUE','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('QUERY_TRACEOUT','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REDO_THREAD_PENDING_WORK','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_CACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_HISTORYCACHE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_SCHEMA_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANFSINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANHASHTABLE_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPL_TRANTEXTINFO_ACCESS','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REPLICA_WRITES','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('REQUEST_FOR_DEADLOCK_SEARCH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESERVED_MEMORY_ALLOCATION_EXT','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE','Memory',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('RESOURCE_SEMAPHORE_QUERY_COMPILE','Compilation',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BPOOL_FLUSH','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_BUFFERPOOL_HELPLW','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_DCOMSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERDBREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERMDREADY','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MASTERUPGRADED','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MEMORYPOOL_ALLOCATEPAGES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_MSDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_RETRY_VIRTUALALLOC','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_SYSTEMTASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TASK','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_DEADLOCK_DETECTION','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_QUANTUM_PUNISHMENT','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_BUFFER_FLUSH','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_BUFFER','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_READ_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_FILE_WRITE_IO_COMPLETION','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_PENDING_BUFFER_WRITERS','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_SHUTDOWN','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLTRACE_WAIT_ENTRIES','Idle',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('THREADPOOL','Worker Thread',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACE_EVTNOTIF','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRACEWRITE','Tracing',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_DT','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_EX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_KP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_NL','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_SH','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRAN_MARKLATCH_UP','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('TRANSACTION_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('UCS_SESSION_REGISTRATION','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_FOR_RESULTS','User Wait',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WAITFOR','User Wait',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITE_COMPLETION','Other Disk IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('WRITELOG','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_OWN_TRANSACTION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACT_RECLAIM_SESSION','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTLOCKINFO','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XACTWORKSPACE_MUTEX','Transaction',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); - END /* IF SELECT SUM(1) FROM ##WaitCategories <> 504 */ - - - - IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL - DROP TABLE #MasterFiles; - CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); - /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;' - ELSE - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;' - EXEC(@StringToExecute); - - IF @FilterPlansByDatabase IS NOT NULL - BEGIN - IF UPPER(LEFT(@FilterPlansByDatabase,4)) = 'USER' - BEGIN - INSERT INTO #FilterPlansByDatabase (DatabaseID) - SELECT database_id - FROM sys.databases - WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb') - END - ELSE - BEGIN - SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' - ;WITH a AS - ( - SELECT CAST(1 AS BIGINT) f, CHARINDEX(',', @FilterPlansByDatabase) t, 1 SEQ - UNION ALL - SELECT t + 1, CHARINDEX(',', @FilterPlansByDatabase, t + 1), SEQ + 1 - FROM a - WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 - ) - INSERT #FilterPlansByDatabase (DatabaseID) - SELECT SUBSTRING(@FilterPlansByDatabase, f, t - f) - FROM a - WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL - OPTION (MAXRECURSION 0) - END - END - - - SET @StockWarningHeader = '', - @StockDetailsHeader = ''; - - /* Get the instance name to use as a Perfmon counter prefix. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) - FROM sys.dm_os_performance_counters; - ELSE - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;' - EXEC(@StringToExecute); - SELECT @ServiceName = object_name FROM #PerfmonStats; - DELETE #PerfmonStats; - END - - /* Build a list of queries that were run in the last 10 seconds. - We're looking for the death-by-a-thousand-small-cuts scenario - where a query is constantly running, and it doesn't have that - big of an impact individually, but it has a ton of impact - overall. We're going to build this list, and then after we - finish our @Seconds sample, we'll compare our plan cache to - this list to see what ran the most. */ - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @CheckProcedureCache = 1 - BEGIN - RAISERROR('@CheckProcedureCache = 1, capturing first pass of plan cache',10,1) WITH NOWAIT; - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) - AND attr.attribute = ''dbid'';'; - END - END - EXEC(@StringToExecute); - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - END /*IF @CheckProcedureCache = 1 */ - - - IF EXISTS (SELECT * - FROM tempdb.sys.all_objects obj - INNER JOIN tempdb.sys.all_columns col1 ON obj.object_id = col1.object_id AND col1.name = 'object_name' - INNER JOIN tempdb.sys.all_columns col2 ON obj.object_id = col2.object_id AND col2.name = 'counter_name' - INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' - WHERE obj.name LIKE '%CustomPerfmonCounters%') - BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters' - EXEC(@StringToExecute); - END - ELSE - BEGIN - /* Add our default Perfmon counters */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL) - /* Below counters added by Jefferson Elias */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL) - /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. - And yes, they actually hard-coded the version numbers into the counters. - For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group - */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL) - END - - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. - After we finish doing our checks, we'll take another sample and compare them. */ - RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - owt.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - 0 AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - os.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_read_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_reads END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_read] END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.io_stall_write_ms END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.num_of_writes END , - CASE @Seconds WHEN 0 THEN 0 ELSE vfs.[num_of_bytes_written] END , - mf.physical_name, - mf.type_desc - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), CASE @Seconds WHEN 0 THEN 0 ELSE dmv.cntr_value END, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; - - - /* Maintenance Tasks Running - Backup Running - CheckID 1 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 1 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Backup Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/backups/' AS URL, - 'Backup 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)) + '. ' - + CASE WHEN COALESCE(s.nt_user_name, s.login_name) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_user_name, s.login_name) + ' ') ELSE '' END AS Details, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'BACKUP%' - AND r.start_time <= DATEADD(minute, -5, GETDATE()); - - /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ - IF @Seconds > 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - BEGIN - SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; - EXEC(@StringToExecute); - END - - - /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ - IF @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%') - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 2 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'DBCC CHECK* Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/dbcc/' AS URL, - 'Corruption check 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) 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, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id - FROM sys.dm_tran_locks l - INNER JOIN sys.databases d ON l.resource_database_id = d.database_id - WHERE l.resource_type = N'DATABASE' - AND l.request_mode = N'S' - AND l.request_status = N'GRANT' - AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t - WHERE r.command LIKE 'DBCC%' - AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%'; - - - /* Maintenance Tasks Running - Restore Running - CheckID 3 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 3 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Restore Running' AS Finding, - 'http://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, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'RESTORE%'; - - - /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 4 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Database File Growing' AS Finding, - 'http://www.BrentOzar.com/go/instant' AS URL, - 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, - 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out http://www.BrentOzar.com/go/instant for details.' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - NULL AS DatabaseID, - NULL AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks t - INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id - INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' - - - /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ - IF @@VERSION NOT LIKE '%Azure%' AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) - BEGIN - SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 5 AS CheckID, - 1 AS Priority, - ''Query Problems'' AS FindingGroup, - ''Long-Running Query Blocking Others'' AS Finding, - ''http://www.BrentOzar.com/go/blocking'' AS URL, - ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows:'' ' - + @LineFeed + @LineFeed + - '+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, - ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, - (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, - COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id)) AS QueryText, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - r.[database_id] AS DatabaseID, - DB_NAME(r.database_id) AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks tBlocked - INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id - LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000;' - EXECUTE sp_executesql @StringToExecute; - END - - /* Query Problems - Plan Cache Erased Recently */ - IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT TOP 1 7 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Plan Cache Erased Recently' AS Finding, - 'http://www.BrentOzar.com/askbrent/plan-cache-erased-recently/' AS URL, - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed - + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed - + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed - + 'SQL Server has to use a lot of CPU power in order to build execution' + @LineFeed - + 'plans and put them in cache again. This causes high CPU loads.' AS Details, - 'Find who did that, and stop them from doing it again.' AS HowToStopIt - FROM sys.dm_exec_query_stats - ORDER BY creation_time - END; - - - /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) - SELECT 8 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Sleeping Query with Open Transactions' AS Finding, - 'http://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, - 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.[host_name] + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_request_end_time AS NVARCHAR(100)) + '. ' AS Details, - 'KILL ' + CAST(s.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - s.last_request_start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, - sessions_with_transactions.open_transaction_count AS OpenTransactionCount - FROM (SELECT session_id, SUM(open_transaction_count) AS open_transaction_count FROM sys.dm_exec_requests WHERE open_transaction_count > 0 GROUP BY session_id) AS sessions_with_transactions - INNER JOIN sys.dm_exec_sessions s ON sessions_with_transactions.session_id = s.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE s.status = 'sleeping' - AND s.last_request_end_time < DATEADD(ss, -10, SYSDATETIME()) - AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.session_id - AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')) - - - /* Query Problems - Query Rolling Back - CheckID 9 */ - IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText) - SELECT 9 AS CheckID, - 1 AS Priority, - 'Query Problems' AS FindingGroup, - 'Query Rolling Back' AS Finding, - 'http://www.BrentOzar.com/askbrent/rollback/' AS URL, - 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, - 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText - FROM sys.dm_exec_sessions s - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - LEFT OUTER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE r.status = 'rollback' - - - /* Server Performance - Page Life Expectancy Low - CheckID 10 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 10 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Page Life Expectancy Low' AS Finding, - 'http://www.BrentOzar.com/askbrent/page-life-expectancy/' AS URL, - 'SQL Server Buffer Manager:Page life expectancy is ' + CAST(c.cntr_value AS NVARCHAR(10)) + ' seconds.' + @LineFeed - + 'This means SQL Server can only keep data pages in memory for that many seconds after reading those pages in from storage.' + @LineFeed - + 'This is a symptom, not a cause - it indicates very read-intensive queries that need an index, or insufficient server memory.' AS Details, - 'Add more memory to the server, or find the queries reading a lot of data, and make them more efficient (or fix them with indexes).' AS HowToStopIt - FROM sys.dm_os_performance_counters c - WHERE object_name LIKE 'SQLServer:Buffer Manager%' - AND counter_name LIKE 'Page life expectancy%' - AND cntr_value < 300 - - /* Server Performance - Too Much Free Memory - CheckID 34 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 34 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Too Much Free Memory' AS Finding, - 'https://BrentOzar.com/go/freememory' AS URL, - CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, - 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt - FROM sys.dm_os_performance_counters cFree - INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' - AND cTotal.counter_name = N'Total Server Memory (KB) ' - WHERE cFree.object_name LIKE N'%Memory Manager%' - AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 - AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) - AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; - - /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 35 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Target Memory Lower Than Max' AS Finding, - 'https://BrentOzar.com/go/target' AS URL, - N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed - + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, - 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt - FROM sys.configurations cMax - INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' - AND cTarget.counter_name = N'Target Server Memory (KB) ' - WHERE cMax.name = 'max server memory (MB)' - AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) - AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ - AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ - - /* Server Info - Database Size, Total GB - CheckID 21 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 21 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Size, Total GB' AS Finding, - CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, - SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM #MasterFiles - WHERE database_id > 4 - - /* Server Info - Database Count - CheckID 22 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 22 AS CheckID, - 251 AS Priority, - 'Server Info' AS FindingGroup, - 'Database Count' AS Finding, - CAST(SUM(1) AS VARCHAR(100)) AS Details, - SUM (1) AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL - FROM sys.databases - WHERE database_id > 4 - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds < 30 - BEGIN - /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - /* Highlight if non SQL processes are using >25% CPU */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - ,record.value('(./Record/SchedulerMonitorEvent/SystemHealth/ProcessUtilization)[1]', 'int') AS SQLUsage - 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 - (y.SQLUsage + y.SystemIdle) >= 25 - - END /* IF @Seconds < 30 */ - - RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; - - - /* End of checks. If we haven't waited @Seconds seconds, wait. */ - IF SYSDATETIMEOFFSET() < @FinishSampleTime - BEGIN - RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; - WAITFOR TIME @FinishSampleTimeWaitFor; - END - - RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - owt.wait_type, - SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - os.wait_type, - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, - SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, - SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os - ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' - ) - GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , - num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - mf.[database_id], - mf.[file_id], - DB_NAME(vfs.database_id) AS [db_name], - mf.name + N' [' + mf.type_desc COLLATE SQL_Latin1_General_CP1_CI_AS + N']' AS file_logical_name , - CAST(( ( vfs.size_on_disk_bytes / 1024.0 ) / 1024.0 ) AS INT) AS size_on_disk_mb , - vfs.io_stall_read_ms , - vfs.num_of_reads , - vfs.[num_of_bytes_read], - vfs.io_stall_write_ms , - vfs.num_of_writes , - vfs.[num_of_bytes_written], - mf.physical_name, - mf.type_desc, - 0, - 0 - FROM sys.dm_io_virtual_file_stats (NULL, NULL) AS vfs - INNER JOIN #MasterFiles AS mf ON vfs.file_id = mf.file_id - AND vfs.database_id = mf.database_id - WHERE vfs.num_of_reads > 0 - OR vfs.num_of_writes > 0; - - INSERT INTO #PerfmonStats (Pass, SampleTime, [object_name],[counter_name],[instance_name],[cntr_value],[cntr_type]) - SELECT 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - RTRIM(dmv.object_name), RTRIM(dmv.counter_name), RTRIM(dmv.instance_name), dmv.cntr_value, dmv.cntr_type - FROM #PerfmonCounters counters - INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS - AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) - - /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ - UPDATE fNow - SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms - WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0 - - UPDATE fNow - SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms - WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0 - - UPDATE pNow - SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, - [value_per_second] = ((1.0 * pNow.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime)) - FROM #PerfmonStats pNow - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pNow.[object_name] AND pFirst.counter_name = pNow.counter_name AND (pFirst.instance_name = pNow.instance_name OR (pFirst.instance_name IS NULL AND pNow.instance_name IS NULL)) - AND pNow.ID > pFirst.ID - WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; - - - /* If we're within 10 seconds of our projected finish time, do the plan cache analysis. */ - IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIME()) > 10 AND @CheckProcedureCache = 1 - BEGIN - - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'http://www.BrentOzar.com/go/topqueries', - 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.') - - END - ELSE IF @CheckProcedureCache = 1 - BEGIN - - - RAISERROR('@CheckProcedureCache = 1, capturing second pass of plan cache',10,1) WITH NOWAIT; - - /* Populate #QueryStats. SQL 2005 doesn't have query hash or query plan hash. */ - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - ELSE - BEGIN - IF @FilterPlansByDatabase IS NULL - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText'; - END - ELSE - BEGIN - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS attr - INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID - WHERE qs.last_execution_time >= @StartSampleTimeText - AND attr.attribute = ''dbid'';'; - END - END - /* Old version pre-2016/06/13: - IF @@VERSION LIKE 'Microsoft SQL Server 2005%' - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - ELSE - SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) - SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 - FROM sys.dm_exec_query_stats qs - WHERE qs.last_execution_time >= @StartSampleTimeText;'; - */ - SET @ParmDefinitions = N'@StartSampleTimeText NVARCHAR(100)'; - SET @Parm1 = CONVERT(NVARCHAR(100), CAST(@StartSampleTime AS DATETIME), 127); - - EXECUTE sp_executesql @StringToExecute, @ParmDefinitions, @StartSampleTimeText = @Parm1; - - RAISERROR('@CheckProcedureCache = 1, totaling up plan cache metrics',10,1) WITH NOWAIT; - - /* Get the totals for the entire plan cache */ - INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) - SELECT 0 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) - FROM sys.dm_exec_query_stats qs; - - - RAISERROR('@CheckProcedureCache = 1, so analyzing execution plans',10,1) WITH NOWAIT; - /* - Pick the most resource-intensive queries to review. Update the Points field - in #QueryStats - if a query is in the top 10 for logical reads, CPU time, - duration, or execution, add 1 to its points. - */ - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_elapsed_time > qsFirst.total_elapsed_time - AND qsNow.Pass = 2 - AND qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - ORDER BY (qsNow.total_elapsed_time - COALESCE(qsFirst.total_elapsed_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_logical_reads > qsFirst.total_logical_reads - AND qsNow.Pass = 2 - AND qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - ORDER BY (qsNow.total_logical_reads - COALESCE(qsFirst.total_logical_reads, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.total_worker_time > qsFirst.total_worker_time - AND qsNow.Pass = 2 - AND qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */ - ORDER BY (qsNow.total_worker_time - COALESCE(qsFirst.total_worker_time, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - WITH qsTop AS ( - SELECT TOP 10 qsNow.ID - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.execution_count > qsFirst.execution_count - AND qsNow.Pass = 2 - AND (qsNow.total_elapsed_time - qsFirst.total_elapsed_time > 1000000 /* Only queries with over 1 second of runtime */ - OR qsNow.total_logical_reads - qsFirst.total_logical_reads > 1000 /* Only queries with over 1000 reads */ - OR qsNow.total_worker_time - qsFirst.total_worker_time > 1000000 /* Only queries with over 1 second of worker time */) - ORDER BY (qsNow.execution_count - COALESCE(qsFirst.execution_count, 0)) DESC) - UPDATE #QueryStats - SET Points = Points + 1 - FROM #QueryStats qs - INNER JOIN qsTop ON qs.ID = qsTop.ID; - - /* Query Stats - CheckID 17 - Most Resource-Intensive Queries */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle) - SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'http://www.BrentOzar.com/go/topqueries', - 'Query stats during the sample:' + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)) AS NVARCHAR(100)) + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes - (COALESCE(qsFirst.total_logical_writes, 0)) AS NVARCHAR(100)) + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time - (COALESCE(qsFirst.total_clr_time, 0)) AS NVARCHAR(100)) + @LineFeed + - @LineFeed + @LineFeed + 'Query stats since ' + CONVERT(NVARCHAR(100), qsNow.creation_time ,121) + @LineFeed + - 'Executions: ' + CAST(qsNow.execution_count AS NVARCHAR(100)) + - CASE qsTotal.execution_count WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time AS NVARCHAR(100)) + - CASE qsTotal.total_elapsed_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CPU Time: ' + CAST(qsNow.total_worker_time AS NVARCHAR(100)) + - CASE qsTotal.total_worker_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Reads: ' + CAST(qsNow.total_logical_reads AS NVARCHAR(100)) + - CASE qsTotal.total_logical_reads WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'Logical Writes: ' + CAST(qsNow.total_logical_writes AS NVARCHAR(100)) + - CASE qsTotal.total_logical_writes WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_logical_writes / qsTotal.total_logical_writes AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - 'CLR Time: ' + CAST(qsNow.total_clr_time AS NVARCHAR(100)) + - CASE qsTotal.total_clr_time WHEN 0 THEN '' ELSE (' - Percent of Server Total: ' + CAST(CAST(100.0 * qsNow.total_clr_time / qsTotal.total_clr_time AS DECIMAL(6,2)) AS NVARCHAR(100)) + '%') END + @LineFeed + - --@LineFeed + @LineFeed + 'Query hash: ' + CAST(qsNow.query_hash AS NVARCHAR(100)) + @LineFeed + - --@LineFeed + @LineFeed + 'Query plan hash: ' + CAST(qsNow.query_plan_hash AS NVARCHAR(100)) + - @LineFeed AS Details, - 'See the URL for tuning tips on why this query may be consuming resources.' AS HowToStopIt, - qp.query_plan, - QueryText = SUBSTRING(st.text, - (qsNow.statement_start_offset / 2) + 1, - ((CASE qsNow.statement_end_offset - WHEN -1 THEN DATALENGTH(st.text) - ELSE qsNow.statement_end_offset - END - qsNow.statement_start_offset) / 2) + 1), - qsNow.ID AS QueryStatsNowID, - qsFirst.ID AS QueryStatsFirstID, - qsNow.plan_handle AS PlanHandle - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st - CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp - WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL - - UPDATE #BlitzFirstResults - SET DatabaseID = CAST(attr.value AS INT), - DatabaseName = DB_NAME(CAST(attr.value AS INT)) - FROM #BlitzFirstResults - CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr - WHERE attr.attribute = 'dbid' - - - END /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ - - - RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; - - /* Wait Stats - CheckID 6 */ - /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT TOP 10 6 AS CheckID, - 200 AS Priority, - 'Wait Stats' AS FindingGroup, - wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ - N'http://www.brentozar.com/sql/wait-stats/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_time_ms > (wBase.wait_time_ms + (.5 * (DATEDIFF(ss,@StartSampleTime,@FinishSampleTime)) * 1000)) /* Only look for things we've actually waited on for half of the time or more */ - ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; - - /* Server Performance - Poison Wait Detected - CheckID 30 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) - SELECT 30 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Poison Wait Detected: ' + wNow.wait_type AS Finding, - N'http://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt - FROM #WaitStats wNow - LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') AND wNow.wait_time_ms > wBase.wait_time_ms; - - - /* Server Performance - Slow Data File Reads - CheckID 11 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 11 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Data File Reads' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these reads: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average read latency during the sample: ' + CAST(((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for data file read speed: 20ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > (fBase.io_stall_read_ms + 1000) - WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'ROWS' - ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; - END - - /* Server Performance - Slow Log File Writes - CheckID 12 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') - BEGIN - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) - SELECT TOP 10 12 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Slow Log File Writes' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, - 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed - + 'File: ' + fNow.PhysicalName + @LineFeed - + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed - + 'Seconds spent waiting on storage for these writes: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / 1000.0) AS NVARCHAR(20)) + @LineFeed - + 'Average write latency during the sample: ' + CAST(((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) ) AS NVARCHAR(20)) + ' milliseconds' + @LineFeed - + 'Microsoft guidance for log file write speed: 3ms or less.' + @LineFeed + @LineFeed AS Details, - 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, - fNow.DatabaseID, - fNow.DatabaseName - FROM #FileStats fNow - INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > (fBase.io_stall_write_ms + 1000) - WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS - AND fNow.TypeDesc = 'LOG' - ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; - END - - - /* SQL Server Internal Maintenance - Log File Growing - CheckID 13 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 13 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Growing' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-growing/' AS URL, - 'Number of growths 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Growths' - AND value_delta > 0 - - - /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 14 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Log File Shrinking' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-shrinking/' AS URL, - 'Number of shrinks 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, - 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - WHERE ps.Pass = 2 - AND object_name = @ServiceName + ':Databases' - AND counter_name = 'Log Shrinks' - AND value_delta > 0 - - /* Query Problems - Compilations/Sec High - CheckID 15 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 15 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/compilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, - 'To find the queries that are compiling, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'If dynamic SQL or non-parameterized strings are involved, consider enabling Forced Parameterization. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Compilations are more than 10% of batch requests per second */ - - /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 16 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Re-Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/recompilations/' AS URL, - 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed - + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed - + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, - 'To find the queries that are being forced to recompile, start with:' + @LineFeed - + 'sp_BlitzCache @SortOrder = ''recent compilations''' + @LineFeed - + 'Examine those plans to find out which objects are changing so quickly that they hit the stats update threshold. See the URL for more details.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':SQL Statistics' AND psComp.counter_name = 'SQL Re-Compilations/sec' AND psComp.value_delta > 0 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Recompilations are more than 10% of batch requests per second */ - - /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 29 AS CheckID, - 40 AS Priority, - 'Table Problems' AS FindingGroup, - 'Forwarded Fetches/Sec High' AS Finding, - 'https://BrentOzar.com/go/fetch/' AS URL, - CAST(ps.value_delta AS NVARCHAR(20)) + ' Forwarded Records (from SQLServer:Access Methods counter)' + @LineFeed - + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, - 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Access Methods' AND psComp.counter_name = 'Forwarded Records/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Access Methods' - AND ps.counter_name = 'Forwarded Records/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - - /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 31 AS CheckID, - 50 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Garbage Collection in Progress' AS Finding, - 'https://BrentOzar.com/go/garbage/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed - + 'This can happen due to memory pressure (causing In-Memory OLTP to shrink its footprint) or' + @LineFeed - + 'due to transactional workloads that constantly insert/delete data.' AS Details, - 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Garbage Collection' - AND ps.counter_name = 'Rows processed/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ - - /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'In-Memory OLTP' AS FindingGroup, - 'Transactions Aborted' AS Finding, - 'https://BrentOzar.com/go/aborted/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed - + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, - 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Transactions' AND psComp.counter_name = 'Transactions aborted/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name LIKE '%XTP Transactions' - AND ps.counter_name = 'Transactions aborted/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 32 AS CheckID, - 100 AS Priority, - 'Query Problems' AS FindingGroup, - 'Suboptimal Plans/Sec High' AS Finding, - 'https://BrentOzar.com/go/suboptimal/' AS URL, - CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed - + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, - 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name = @ServiceName + ':Workload GroupStats' AND psComp.counter_name = 'Suboptimal plans/sec' AND psComp.value_delta > 100 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':Workload GroupStats' - AND ps.counter_name = 'Suboptimal plans/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ - - - - /* Server Info - Batch Requests per Sec - CheckID 19 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Batch Requests per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'Batch Requests/sec'; - - - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - - /* Server Info - SQL Compilations/sec - CheckID 25 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 25 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Compilations/sec'; - - /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 26 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Re-Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Re-Compilations/sec'; - - /* Server Info - Wait Time per Core per Sec - CheckID 20 */ - IF @Seconds > 0 - BEGIN - WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), - waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), - cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'Wait Time per Core per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST((waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS NVARCHAR(20)) AS Details, - (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS DetailsInt - FROM cores i - CROSS JOIN waits1 - CROSS JOIN waits2; - END - - /* Server Performance - High CPU Utilization CheckID 24 */ - IF @Seconds >= 30 - BEGIN - /* If we're waiting 30+ seconds, run this check at the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 - - END /* IF @Seconds < 30 */ - - - /* If we didn't find anything, apologize. */ - IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) - BEGIN - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 1 , - 'No Problems Found' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' - ); - - END /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ - - /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - VALUES ( -1 , - 255 , - 'Thanks!' , - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'To get help or add your own contributions, join us at http://FirstResponderKit.org.' - ); - - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - - ) - VALUES ( -1 , - 0 , - 'sp_BlitzFirst ' + CAST(CONVERT(DATETIMEOFFSET, @VersionDate, 102) AS VARCHAR(100)), - 'From Your Community Volunteers' , - 'http://FirstResponderKit.org/' , - 'We hope you found this tool useful.' - ); - - /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old */ - IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 27 AS CheckID , - 0 AS Priority , - 'Outdated sp_BlitzFirst' AS FindingsGroup , - 'sp_BlitzFirst is Over 6 Months Old' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details - END - - RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; - - - /* If they want to run sp_BlitzCache and export to table, go for it. */ - IF @OutputTableNameBlitzCache IS NOT NULL - AND @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; - - /* Set the sp_BlitzCache sort order based on their top wait type */ - - /* First, check for poison waits - CheckID 30 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 30) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE_QUERY_COMPILE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: THREADPOOL' THEN 'executions' - WHEN Finding = 'Poison Wait Detected: LOG_RATE_GOVERNOR' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_CATCHUP_THROTTLE' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_COMMIT_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_ROLLBACK_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_SLOW_SECONDARY_THROTTLE' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 30 - ORDER BY DetailsInt DESC; - END - - /* Too much free memory - which probably indicates queries finished w/huge grants - CheckID 34 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 34) - SET @BlitzCacheSortOrder = 'memory grant'; - - /* Next, Compilations/Sec High - CheckID 15 and 16 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID IN (15,16)) - SET @BlitzCacheSortOrder = 'compilations'; - - /* Still not set? Use the top wait type. */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 6) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'ASYNC_NETWORK_IO' THEN 'duration' - WHEN Finding = 'CXPACKET' THEN 'reads' - WHEN Finding = 'LATCH_EX' THEN 'reads' - WHEN Finding LIKE 'LCK%' THEN 'duration' - WHEN Finding LIKE 'PAGEIOLATCH%' THEN 'reads' - WHEN Finding = 'SOS_SCHEDULER_YIELD' THEN 'cpu' - WHEN Finding = 'WRITELOG' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 6 - ORDER BY DetailsInt DESC; - END - /* Still null? Just use the default. */ - - - - /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ - IF EXISTS (SELECT * FROM sys.objects o - INNER JOIN sys.parameters pMB ON o.object_id = pMB.object_id AND pMB.name = '@MinutesBack' - INNER JOIN sys.parameters pCDO ON o.object_id = pCDO.object_id AND pCDO.name = '@CheckDateOverride' - WHERE o.name = 'sp_BlitzCache') - BEGIN - /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; - EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; - - /* If there's no data, let's just analyze the last 15 minutes of the plan cache */ - IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 - SET @BlitzCacheMinutesBack = 15; - - IF @BlitzCacheSortOrder IS NOT NULL - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @SortOrder = @BlitzCacheSortOrder, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - ELSE - EXEC sp_BlitzCache - @OutputDatabaseName = @UnquotedOutputDatabaseName, - @OutputSchemaName = @UnquotedOutputSchemaName, - @OutputTableName = @OutputTableNameBlitzCache, - @CheckDateOverride = @StartSampleTime, - @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - - ELSE /* No sp_BlitzCache found, or it's outdated */ - BEGIN - INSERT INTO #BlitzFirstResults - ( CheckID , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 36 AS CheckID , - 0 AS Priority , - 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , - 'Update Your sp_BlitzCache' AS Finding , - 'http://FirstResponderKit.org/' AS URL , - 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details - END - - RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; - - END /* End running sp_BlitzCache */ - - /* @OutputTableName lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND @OutputTableName NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - - EXEC(@StringToExecute); - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + (CONVERT(NVARCHAR(100), @StartSampleTime, 121)) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableName - + ''') IS NULL) CREATE TABLE ' - + @OutputTableName - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - CheckID INT NOT NULL, - Priority TINYINT NOT NULL, - FindingsGroup VARCHAR(50) NOT NULL, - Finding VARCHAR(200) NOT NULL, - URL VARCHAR(200) NOT NULL, - Details NVARCHAR(4000) NULL, - HowToStopIt [XML] NULL, - QueryPlan [XML] NULL, - QueryText NVARCHAR(MAX) NULL, - StartTime DATETIMEOFFSET NULL, - LoginName NVARCHAR(128) NULL, - NTUserName NVARCHAR(128) NULL, - OriginalLoginName NVARCHAR(128) NULL, - ProgramName NVARCHAR(128) NULL, - HostName NVARCHAR(128) NULL, - DatabaseID INT NULL, - DatabaseName NVARCHAR(128) NULL, - OpenTransactionCount INT NULL, - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - /* @OutputTableNameFileStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameFileStats IS NOT NULL - AND @OutputTableNameFileStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameFileStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats_View + ' AS ' + @LineFeed - + 'SELECT f.ServerName, f.CheckDate, f.DatabaseID, f.DatabaseName, f.FileID, f.FileLogicalName, f.TypeDesc, f.PhysicalName, f.SizeOnDiskMB' + @LineFeed - + ', DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth' + @LineFeed - + ', (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms' + @LineFeed - + ', io_stall_read_ms_average = CASE WHEN (f.num_of_reads - fPrior.num_of_reads) = 0 THEN 0 ELSE (f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads) END' + @LineFeed - + ', (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads' + @LineFeed - + ', (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read' + @LineFeed - + ', (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms' + @LineFeed - + ', io_stall_write_ms_average = CASE WHEN (f.num_of_writes - fPrior.num_of_writes) = 0 THEN 0 ELSE (f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes) END' + @LineFeed - + ', (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes' + @LineFeed - + ', (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName AND f.DatabaseID = fPrior.DatabaseID AND f.FileID = fPrior.FileID AND f.CheckDate > fPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fMiddle ON f.ServerName = fMiddle.ServerName AND f.DatabaseID = fMiddle.DatabaseID AND f.FileID = fMiddle.FileID AND f.CheckDate > fMiddle.CheckDate AND fMiddle.CheckDate > fPrior.CheckDate' + @LineFeed - + 'WHERE fMiddle.ID IS NULL AND f.num_of_reads >= fPrior.num_of_reads AND f.num_of_writes >= fPrior.num_of_writes - AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameFileStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameFileStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameFileStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - DatabaseID INT NOT NULL, - FileID INT NOT NULL, - DatabaseName NVARCHAR(256) , - FileLogicalName NVARCHAR(256) , - TypeDesc NVARCHAR(60) , - SizeOnDiskMB BIGINT , - io_stall_read_ms BIGINT , - num_of_reads BIGINT , - bytes_read BIGINT , - io_stall_write_ms BIGINT , - num_of_writes BIGINT , - bytes_written BIGINT, - PhysicalName NVARCHAR(520) , - DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNamePerfmonStats IS NOT NULL - AND @OutputTableNamePerfmonStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNamePerfmonStats + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); - - /* Create the view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed - + 'SELECT p.ServerName, p.CheckDate, p.object_name, p.counter_name, p.instance_name' + @LineFeed - + ', DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', p.cntr_value' + @LineFeed - + ', p.cntr_type' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) AS cntr_delta' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) * 1.0 / DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS cntr_delta_per_second' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' p' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pPrior ON p.ServerName = pPrior.ServerName AND p.object_name = pPrior.object_name AND p.counter_name = pPrior.counter_name AND p.instance_name = pPrior.instance_name AND p.CheckDate > pPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pMiddle ON p.ServerName = pMiddle.ServerName AND p.object_name = pMiddle.object_name AND p.counter_name = pMiddle.counter_name AND p.instance_name = pMiddle.instance_name AND p.CheckDate > pMiddle.CheckDate AND pMiddle.CheckDate > pPrior.CheckDate' + @LineFeed - + 'WHERE pMiddle.ID IS NULL AND DATEDIFF(MI, pPrior.CheckDate, p.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END; - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNamePerfmonStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - - - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNamePerfmonStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNamePerfmonStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - [object_name] NVARCHAR(128) NOT NULL, - [counter_name] NVARCHAR(128) NOT NULL, - [instance_name] NVARCHAR(128) NULL, - [cntr_value] BIGINT NULL, - [cntr_type] INT NOT NULL, - [value_delta] BIGINT NULL, - [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - /* @OutputTableNameWaitStats lets us export the results to a permanent table */ - IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableNameWaitStats IS NOT NULL - AND @OutputTableNameWaitStats NOT LIKE '#%' - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - /* Create the table */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableNameWaitStats + ''') ' + @LineFeed - + 'BEGIN' + @LineFeed - + 'CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID));' + @LineFeed - + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed - + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed - + 'END' - - EXEC(@StringToExecute); - - /* Create the wait stats category table */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_Categories; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')' - EXEC(@StringToExecute); - END - - /* Make sure the wait stats category table has the current number of rows */ - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''IF (SELECT COALESCE(SUM(1),0) FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ') <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories)' + @LineFeed - + 'BEGIN ' + @LineFeed - + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed - + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed - + 'END'')' - EXEC(@StringToExecute); - - - /* Create the wait stats view */ - SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; - IF OBJECT_ID(@ObjectFullName) IS NULL - BEGIN - SET @StringToExecute = 'USE ' - + @OutputDatabaseName - + '; EXEC (''CREATE VIEW ' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed - + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed - + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 60000.0 AS wait_time_minutes_delta' + @LineFeed - + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed - + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed - + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND w.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wMiddle ON w.ServerName = wMiddle.ServerName AND w.wait_type = wMiddle.wait_type AND w.CheckDate > wMiddle.CheckDate AND wMiddle.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed - + 'WHERE wMiddle.ID IS NULL AND w.wait_time_ms >= wPrior.wait_time_ms AND DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END - - - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - - /* Delete history older than @OutputTableRetentionDays */ - SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') DELETE ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableNameWaitStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' - + @OutputTableNameWaitStats - + ''') IS NULL) CREATE TABLE ' - + @OutputTableNameWaitStats - + ' (ID INT IDENTITY(1,1) NOT NULL, - ServerName NVARCHAR(128), - CheckDate DATETIMEOFFSET, - wait_type NVARCHAR(60), - wait_time_ms BIGINT, - signal_wait_time_ms BIGINT, - waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - + ' INSERT ' - + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - END - ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - - - - - DECLARE @separator AS VARCHAR(1); - IF @OutputType = 'RSV' - SET @separator = CHAR(31); - ELSE - SET @separator = ','; - - IF @OutputType = 'COUNT' AND @SinceStartup = 0 - BEGIN - SELECT COUNT(*) AS Warnings - FROM #BlitzFirstResults - END - ELSE - IF @OutputType = 'Opserver1' AND @SinceStartup = 0 - BEGIN - - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - r.[Details], - r.[HowToStopIt] , - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; - END - ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 - BEGIN - - SELECT Result = CAST([Priority] AS NVARCHAR(100)) - + @separator + CAST(CheckID AS NVARCHAR(100)) - + @separator + COALESCE([FindingsGroup], - '(N/A)') + @separator - + COALESCE([Finding], '(N/A)') + @separator - + COALESCE(DatabaseName, '(N/A)') + @separator - + COALESCE([URL], '(N/A)') + @separator - + COALESCE([Details], '(N/A)') - FROM #BlitzFirstResults - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - Details; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - [QueryText], - [QueryPlan] - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 - BEGIN - SELECT [Priority] , - [FindingsGroup] , - [Finding] , - [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS NVARCHAR(MAX)) AS Details, - CAST([HowToStopIt] AS NVARCHAR(MAX)) AS HowToStopIt, - CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, - CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan - FROM #BlitzFirstResults - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY Priority , - FindingsGroup , - CASE - WHEN CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - Finding, - ID; - END - ELSE IF @ExpertMode = 1 - BEGIN - IF @SinceStartup = 0 - SELECT r.[Priority] , - r.[FindingsGroup] , - r.[Finding] , - r.[URL] , - CAST(@StockDetailsHeader + r.[Details] + @StockDetailsFooter AS XML) AS Details, - CAST(@StockWarningHeader + r.HowToStopIt + @StockWarningFooter AS XML) AS HowToStopIt, - r.[CheckID] , - r.[StartTime], - r.[LoginName], - r.[NTUserName], - r.[OriginalLoginName], - r.[ProgramName], - r.[HostName], - r.[DatabaseID], - r.[DatabaseName], - r.[OpenTransactionCount], - r.[QueryPlan], - r.[QueryText], - qsNow.plan_handle AS PlanHandle, - qsNow.sql_handle AS SqlHandle, - qsNow.statement_start_offset AS StatementStartOffset, - qsNow.statement_end_offset AS StatementEndOffset, - [Executions] = qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)), - [ExecutionsPercent] = CAST(100.0 * (qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0))) / (qsTotal.execution_count - qsTotalFirst.execution_count) AS DECIMAL(6,2)), - [Duration] = qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)), - [DurationPercent] = CAST(100.0 * (qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0))) / (qsTotal.total_elapsed_time - qsTotalFirst.total_elapsed_time) AS DECIMAL(6,2)), - [CPU] = qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0)), - [CPUPercent] = CAST(100.0 * (qsNow.total_worker_time - (COALESCE(qsFirst.total_worker_time, 0))) / (qsTotal.total_worker_time - qsTotalFirst.total_worker_time) AS DECIMAL(6,2)), - [Reads] = qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0)), - [ReadsPercent] = CAST(100.0 * (qsNow.total_logical_reads - (COALESCE(qsFirst.total_logical_reads, 0))) / (qsTotal.total_logical_reads - qsTotalFirst.total_logical_reads) AS DECIMAL(6,2)), - [PlanCreationTime] = CONVERT(NVARCHAR(100), qsNow.creation_time ,121), - [TotalExecutions] = qsNow.execution_count, - [TotalExecutionsPercent] = CAST(100.0 * qsNow.execution_count / qsTotal.execution_count AS DECIMAL(6,2)), - [TotalDuration] = qsNow.total_elapsed_time, - [TotalDurationPercent] = CAST(100.0 * qsNow.total_elapsed_time / qsTotal.total_elapsed_time AS DECIMAL(6,2)), - [TotalCPU] = qsNow.total_worker_time, - [TotalCPUPercent] = CAST(100.0 * qsNow.total_worker_time / qsTotal.total_worker_time AS DECIMAL(6,2)), - [TotalReads] = qsNow.total_logical_reads, - [TotalReadsPercent] = CAST(100.0 * qsNow.total_logical_reads / qsTotal.total_logical_reads AS DECIMAL(6,2)), - r.[DetailsInt] - FROM #BlitzFirstResults r - LEFT OUTER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 - LEFT OUTER JOIN #QueryStats qsTotalFirst ON qsTotalFirst.Pass = -1 - LEFT OUTER JOIN #QueryStats qsNow ON r.QueryStatsNowID = qsNow.ID - LEFT OUTER JOIN #QueryStats qsFirst ON r.QueryStatsFirstID = qsFirst.ID - WHERE (@Seconds > 0 OR (Priority IN (0, 250, 251, 255))) /* For @Seconds = 0, filter out broken checks for now */ - ORDER BY r.Priority , - r.FindingsGroup , - CASE - WHEN r.CheckID = 6 THEN DetailsInt - ELSE 0 - END DESC, - r.Finding, - r.ID; - - ------------------------- - --What happened: #WaitStats - ------------------------- - IF @Seconds = 0 - BEGIN - /* Measure waits in hours */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - CAST(c.[Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / 60 / 60 / 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; - END - ELSE - BEGIN - /* Measure waits in seconds */ - ;WITH max_batch AS ( - SELECT MAX(SampleTime) AS SampleTime - FROM #WaitStats - ) - SELECT - 'WAIT STATS' AS Pattern, - b.SampleTime AS [Sample Ended], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], - wd1.wait_type, - COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - c.[Wait Time (Seconds)], - 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 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores - CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c - LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType - WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 - AND wd2.wait_time_ms-wd1.wait_time_ms > 0 - ORDER BY [Wait Time (Seconds)] DESC; - END; - - ------------------------- - --What happened: #FileStats - ------------------------- - WITH readstats AS ( - SELECT 'PHYSICAL READS' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_reads - wd1.num_of_reads ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_reads - wd1.num_of_reads > 0 - THEN CAST(( wd2.bytes_read - wd1.bytes_read)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_read_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ), - writestats AS ( - SELECT - 'PHYSICAL WRITES' AS Pattern, - ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_write_ms DESC) AS StallRank, - wd2.SampleTime AS [Sample Time], - DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Sample (seconds)], - wd1.DatabaseName , - wd1.FileLogicalName AS [File Name], - UPPER(SUBSTRING(wd1.PhysicalName, 1, 2)) AS [Drive] , - wd1.SizeOnDiskMB , - ( wd2.num_of_writes - wd1.num_of_writes ) AS [# Reads/Writes], - CASE WHEN wd2.num_of_writes - wd1.num_of_writes > 0 - THEN CAST(( wd2.bytes_written - wd1.bytes_written)/1024./1024. AS NUMERIC(21,1)) - ELSE 0 - END AS [MB Read/Written], - wd2.avg_stall_write_ms AS [Avg Stall (ms)], - wd1.PhysicalName AS [file physical name] - FROM #FileStats wd2 - JOIN #FileStats wd1 ON wd2.SampleTime > wd1.SampleTime - AND wd1.DatabaseID = wd2.DatabaseID - AND wd1.FileID = wd2.FileID - ) - SELECT - Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM readstats - WHERE StallRank <=5 AND [MB Read/Written] > 0 - UNION ALL - SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] - FROM writestats - WHERE StallRank <=5 AND [MB Read/Written] > 0; - - - ------------------------- - --What happened: #PerfmonStats - ------------------------- - - SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, - pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, - pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, - pLast.cntr_value - pFirst.cntr_value AS ValueDelta, - ((1.0 * pLast.cntr_value - pFirst.cntr_value) / DATEDIFF(ss, pFirst.SampleTime, pLast.SampleTime)) AS ValuePerSecond - FROM #PerfmonStats pLast - INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) - AND pLast.ID > pFirst.ID - WHERE pLast.cntr_value <> pFirst.cntr_value - ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name - - - ------------------------- - --What happened: #QueryStats - ------------------------- - IF @CheckProcedureCache = 1 - BEGIN - - SELECT qsNow.*, qsFirst.* - FROM #QueryStats qsNow - INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.Pass = 2 - END - ELSE - BEGIN - SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details] - END - END - - DROP TABLE #BlitzFirstResults; - - /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 -IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - BEGIN - IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL - BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END - ELSE - BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - -END /* IF @LogMessage IS NULL */ -END /* ELSE IF @OutputType = 'SCHEMA' */ - -SET NOCOUNT OFF; -GO - - - -/* How to run it: -EXEC dbo.sp_BlitzFirst - -With extra diagnostic info: -EXEC dbo.sp_BlitzFirst @ExpertMode = 1; - -Saving output to tables: -EXEC sp_BlitzFirst -, @OutputDatabaseName = 'DBAtools' -, @OutputSchemaName = 'dbo' -, @OutputTableName = 'BlitzFirst' -, @OutputTableNameFileStats = 'BlitzFirst_FileStats' -, @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' -, @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' -, @OutputTableNameBlitzCache = 'BlitzCache' -*/ -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzIndex AS RETURN 0;') -GO - -ALTER PROCEDURE dbo.sp_BlitzIndex - @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.*/ - @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 */ - /*Note:@Filter doesn't do anything unless @Mode=0*/ - @SkipPartitions BIT = 0, - @SkipStatistics BIT = 1, - @GetAllDatabases BIT = 0, - @BringThePain BIT = 0, - @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , - @Help TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - - -IF @Help = 1 PRINT ' -/* -sp_BlitzIndex from http://FirstResponderKit.org - -This script analyzes the design and performance of your indexes. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - The @OutputDatabaseName parameters are not functional yet. To check the - status of this enhancement request, visit: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/221 - - Does not analyze columnstore, spatial, XML, or full text indexes. If you - would like to contribute code to analyze those, head over to Github and - check out the issues list: http://FirstResponderKit.org - - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. - -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important - for the user to understand if it is going to be offline and not just run a script. - -- Example 2: they do not include all the options the index may have been created with (padding, compression - filegroup/partition scheme etc.) - -- (The compression and filegroup index create syntax is not trivial because it is set at the partition - level and is not trivial to code.) - - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of insanity.) - -Unknown limitations of this version: - - We knew them once, but we forgot. - -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/milestone/4?closed=1 - - -MIT License - -Copyright (c) 2016 Brent Ozar Unlimited - -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. -' - - -DECLARE @ScriptVersionName NVARCHAR(50); -DECLARE @DaysUptime NUMERIC(23,2); -DECLARE @DatabaseID INT; -DECLARE @ObjectID INT; -DECLARE @dsql NVARCHAR(MAX); -DECLARE @params NVARCHAR(MAX); -DECLARE @msg NVARCHAR(4000); -DECLARE @ErrorSeverity INT; -DECLARE @ErrorState INT; -DECLARE @Rowcount BIGINT; -DECLARE @SQLServerProductVersion NVARCHAR(128); -DECLARE @SQLServerEdition INT; -DECLARE @FilterMB INT; -DECLARE @collation NVARCHAR(256); -DECLARE @NumDatabases INT; -DECLARE @LineFeed NVARCHAR(5); - -SET @LineFeed = CHAR(13) + CHAR(10); -SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ -SET @FilterMB=250; -SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate) - -RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; - -IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL - DROP TABLE #IndexSanity; - -IF OBJECT_ID('tempdb..#IndexPartitionSanity') IS NOT NULL - DROP TABLE #IndexPartitionSanity; - -IF OBJECT_ID('tempdb..#IndexSanitySize') IS NOT NULL - DROP TABLE #IndexSanitySize; - -IF OBJECT_ID('tempdb..#IndexColumns') IS NOT NULL - DROP TABLE #IndexColumns; - -IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL - DROP TABLE #MissingIndexes; - -IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL - DROP TABLE #ForeignKeys; - -IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL - DROP TABLE #BlitzIndexResults; - -IF OBJECT_ID('tempdb..#IndexCreateTsql') IS NOT NULL - DROP TABLE #IndexCreateTsql; - -IF OBJECT_ID('tempdb..#DatabaseList') IS NOT NULL - DROP TABLE #DatabaseList; - -IF OBJECT_ID('tempdb..#Statistics') IS NOT NULL - DROP TABLE #Statistics; - -IF OBJECT_ID('tempdb..#PartitionCompressionInfo') IS NOT NULL - DROP TABLE #PartitionCompressionInfo; - -IF OBJECT_ID('tempdb..#ComputedColumns') IS NOT NULL - DROP TABLE #ComputedColumns; - -IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL - DROP TABLE #TraceStatus; - -IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL - DROP TABLE #TemporalTables; - - RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; - CREATE TABLE #BlitzIndexResults - ( - blitz_result_id INT IDENTITY PRIMARY KEY, - check_id INT NOT NULL, - index_sanity_id INT NULL, - Priority INT NULL, - findings_group VARCHAR(4000) NOT NULL, - finding VARCHAR(200) NOT NULL, - [database_name] VARCHAR(200) NULL, - URL VARCHAR(200) NOT NULL, - details NVARCHAR(4000) NOT NULL, - index_definition NVARCHAR(MAX) NOT NULL, - secret_columns NVARCHAR(MAX) NULL, - index_usage_summary NVARCHAR(MAX) NULL, - index_size_summary NVARCHAR(MAX) NULL, - create_tsql NVARCHAR(MAX) NULL, - more_info NVARCHAR(MAX)NULL - ); - - CREATE TABLE #IndexSanity - ( - [index_sanity_id] INT IDENTITY PRIMARY KEY CLUSTERED, - [database_id] SMALLINT NOT NULL , - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [index_type] TINYINT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [object_name] NVARCHAR(128) NOT NULL , - index_name NVARCHAR(128) NULL , - key_column_names NVARCHAR(MAX) NULL , - key_column_names_with_sort_order NVARCHAR(MAX) NULL , - key_column_names_with_sort_order_no_types NVARCHAR(MAX) NULL , - count_key_columns INT NULL , - include_column_names NVARCHAR(MAX) NULL , - include_column_names_no_types NVARCHAR(MAX) NULL , - count_included_columns INT NULL , - partition_key_column_name NVARCHAR(MAX) NULL, - filter_definition NVARCHAR(MAX) NOT NULL , - is_indexed_view BIT NOT NULL , - is_unique BIT NOT NULL , - is_primary_key BIT NOT NULL , - is_XML BIT NOT NULL, - is_spatial BIT NOT NULL, - is_NC_columnstore BIT NOT NULL, - is_CX_columnstore BIT NOT NULL, - is_disabled BIT NOT NULL , - is_hypothetical BIT NOT NULL , - is_padded BIT NOT NULL , - fill_factor SMALLINT NOT NULL , - user_seeks BIGINT NOT NULL , - user_scans BIGINT NOT NULL , - user_lookups BIGINT NOT NULL , - user_updates BIGINT NULL , - last_user_seek DATETIME NULL , - last_user_scan DATETIME NULL , - last_user_lookup DATETIME NULL , - last_user_update DATETIME NULL , - is_referenced_by_foreign_key BIT DEFAULT(0), - secret_columns NVARCHAR(MAX) NULL, - count_secret_columns INT NULL, - create_date DATETIME NOT NULL, - modify_date DATETIME NOT NULL, - [db_schema_object_name] AS [schema_name] + '.' + [object_name] , - [db_schema_object_indexid] AS [schema_name] + '.' + [object_name] - + CASE WHEN [index_name] IS NOT NULL THEN '.' + index_name - ELSE '' - END + ' (' + CAST(index_id AS NVARCHAR(20)) + ')' , - first_key_column_name AS CASE WHEN count_key_columns > 1 - THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) - ELSE key_column_names - END , - index_definition AS - CASE WHEN partition_key_column_name IS NOT NULL - THEN N'[PARTITIONED BY:' + partition_key_column_name + N']' - ELSE '' - END + - CASE index_id - WHEN 0 THEN N'[HEAP] ' - WHEN 1 THEN N'[CX] ' - ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN '[VIEW] ' - ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' - 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_disabled = 1 THEN N'[DISABLED] ' - ELSE N'' END + CASE WHEN is_hypothetical = 1 THEN N'[HYPOTHETICAL] ' - ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 THEN N'[UNIQUE] ' - ELSE N'' END + CASE WHEN count_key_columns > 0 THEN - N'[' + CAST(count_key_columns AS VARCHAR(10)) + N' KEY' - + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + LTRIM(key_column_names_with_sort_order) - ELSE N'' END + CASE WHEN count_included_columns > 0 THEN - N' [' + CAST(count_included_columns AS VARCHAR(10)) + N' INCLUDE' + - + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END - + N'] ' + include_column_names - ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition - ELSE N'' END , - [total_reads] AS user_seeks + user_scans + user_lookups, - [reads_per_write] AS CAST(CASE WHEN user_updates > 0 - THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) - ELSE 0 END AS MONEY) , - [index_usage_summary] AS N'Reads: ' + - REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), '.00', '') - + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN - N' (' - + RTRIM( - CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), '.00', '') + N' seek ' ELSE N'' END - + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), '.00', '') + N' scan ' ELSE N'' END - + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), '.00', '') + N' lookup' ELSE N'' END - ) - + N') ' - ELSE N' ' END - + N'Writes:' + - REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), '.00', ''), - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([object_name],'''') + N';' - ); - RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; - IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') - CREATE UNIQUE INDEX uq_database_id_object_id_index_id ON #IndexSanity (database_id, object_id, index_id); - - - CREATE TABLE #IndexPartitionSanity - ( - [index_partition_sanity_id] INT IDENTITY, - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL , - [object_id] INT NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL, - [index_id] INT NOT NULL , - [partition_number] INT NOT NULL , - row_count BIGINT NOT NULL , - reserved_MB NUMERIC(29,2) NOT NULL , - reserved_LOB_MB NUMERIC(29,2) NOT NULL , - reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - leaf_insert_count BIGINT NULL , - leaf_delete_count BIGINT NULL , - leaf_update_count BIGINT NULL , - range_scan_count BIGINT NULL , - singleton_lookup_count BIGINT NULL , - forwarded_fetch_count BIGINT NULL , - lob_fetch_in_pages BIGINT NULL , - lob_fetch_in_bytes BIGINT NULL , - row_overflow_fetch_in_pages BIGINT NULL , - row_overflow_fetch_in_bytes BIGINT NULL , - row_lock_count BIGINT NULL , - row_lock_wait_count BIGINT NULL , - row_lock_wait_in_ms BIGINT NULL , - page_lock_count BIGINT NULL , - page_lock_wait_count BIGINT NULL , - page_lock_wait_in_ms BIGINT NULL , - index_lock_promotion_attempt_count BIGINT NULL , - index_lock_promotion_count BIGINT NULL, - data_compression_desc VARCHAR(60) NULL - ); - - CREATE TABLE #IndexSanitySize - ( - [index_sanity_size_id] INT IDENTITY NOT NULL , - [index_sanity_id] INT NULL , - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128) NOT NULL, - partition_count INT NOT NULL , - total_rows BIGINT NOT NULL , - total_reserved_MB NUMERIC(29,2) NOT NULL , - total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , - total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , - total_leaf_delete_count BIGINT NULL, - total_leaf_update_count BIGINT NULL, - total_range_scan_count BIGINT NULL, - total_singleton_lookup_count BIGINT NULL, - total_forwarded_fetch_count BIGINT NULL, - total_row_lock_count BIGINT NULL , - total_row_lock_wait_count BIGINT NULL , - total_row_lock_wait_in_ms BIGINT NULL , - avg_row_lock_wait_in_ms BIGINT NULL , - total_page_lock_count BIGINT NULL , - total_page_lock_wait_count BIGINT NULL , - total_page_lock_wait_in_ms BIGINT NULL , - avg_page_lock_wait_in_ms BIGINT NULL , - total_index_lock_promotion_attempt_count BIGINT NULL , - total_index_lock_promotion_count BIGINT NULL , - data_compression_desc VARCHAR(8000) NULL, - index_size_summary AS ISNULL( - CASE WHEN partition_count > 1 - THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' - ELSE N'' - END + REPLACE(CONVERT(NVARCHAR(30),CAST([total_rows] AS MONEY), 1), N'.00', N'') + N' rows; ' - + CASE WHEN total_reserved_MB > 1024 THEN - CAST(CAST(total_reserved_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB' - ELSE - CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' - END - + CASE WHEN total_reserved_LOB_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB LOB' - WHEN total_reserved_LOB_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB LOB' - ELSE '' - END - + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Row Overflow' - WHEN total_reserved_row_overflow_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' - ELSE '' - END , - N'Error- NULL in computed column'), - index_op_stats AS ISNULL( - ( - REPLACE(CONVERT(NVARCHAR(30),CAST(total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_range_scan_count AS MONEY), 1),N'.00',N'') + N' scans/seeks; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_delete_count AS MONEY), 1),N'.00',N'') + N' deletes; ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(total_leaf_update_count AS MONEY), 1),N'.00',N'') + N' updates; ' - + CASE WHEN ISNULL(total_forwarded_fetch_count,0) >0 THEN - REPLACE(CONVERT(NVARCHAR(30),CAST(total_forwarded_fetch_count AS MONEY), 1),N'.00',N'') + N' forward records fetched; ' - ELSE N'' END - - /* rows will only be in this dmv when data is in memory for the table */ - ), N'Table metadata not in memory'), - index_lock_wait_summary AS ISNULL( - CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND - total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits.' - ELSE - CASE WHEN total_row_lock_wait_count > 0 THEN - N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_row_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_row_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_row_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_page_lock_wait_count > 0 THEN - N'Page lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_page_lock_wait_count AS MONEY), 1), N'.00', N'') - + N'; total duration: ' + - CASE WHEN total_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((total_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - + N'avg duration: ' + - CASE WHEN avg_page_lock_wait_in_ms >= 60000 THEN /*More than 1 min*/ - REPLACE(CONVERT(NVARCHAR(30),CAST((avg_page_lock_wait_in_ms/60000) AS MONEY), 1), N'.00', N'') + N' minutes; ' - ELSE - REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(avg_page_lock_wait_in_ms/1000,0) AS MONEY), 1), N'.00', N'') + N' seconds; ' - END - ELSE N'' - END + - CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN - N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') - + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') + N'.' - ELSE N'' - END - END - ,'Error- NULL in computed column') - ); - - CREATE TABLE #IndexColumns - ( - [database_id] INT NOT NULL, - [schema_name] NVARCHAR(128), - [object_id] INT NOT NULL , - [index_id] INT NOT NULL , - [key_ordinal] INT NULL , - is_included_column BIT NULL , - is_descending_key BIT NULL , - [partition_ordinal] INT NULL , - column_name NVARCHAR(256) NOT NULL , - system_type_name NVARCHAR(256) NOT NULL, - max_length SMALLINT NOT NULL, - [precision] TINYINT NOT NULL, - [scale] TINYINT NOT NULL, - collation_name NVARCHAR(256) NULL, - is_nullable BIT NULL, - is_identity BIT NULL, - is_computed BIT NULL, - is_replicated BIT NULL, - is_sparse BIT NULL, - is_filestream BIT NULL, - seed_value BIGINT NULL, - increment_value INT NULL , - last_value BIGINT NULL, - is_not_for_replication BIT NULL - ); - CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns - (database_id, object_id, index_id); - - CREATE TABLE #MissingIndexes - ([database_id] INT NOT NULL, - [object_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - [table_name] NVARCHAR(128), - [statement] NVARCHAR(512) NOT NULL, - magic_benefit_number AS (( user_seeks + user_scans ) * avg_total_user_cost * avg_user_impact), - avg_total_user_cost NUMERIC(29,4) NOT NULL, - avg_user_impact NUMERIC(29,1) NOT NULL, - user_seeks BIGINT NOT NULL, - user_scans BIGINT NOT NULL, - unique_compiles BIGINT NULL, - equality_columns NVARCHAR(4000), - inequality_columns NVARCHAR(4000), - included_columns NVARCHAR(4000), - is_low BIT, - [index_estimated_impact] AS - REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (user_seeks + user_scans) - AS BIGINT) AS MONEY), 1), '.00', '') + N' use' - + CASE WHEN (user_seeks + user_scans) > 1 THEN N's' ELSE N'' END - +N'; Impact: ' + CAST(avg_user_impact AS NVARCHAR(30)) - + N'%; Avg query cost: ' - + CAST(avg_total_user_cost AS NVARCHAR(30)), - [missing_index_details] AS - CASE WHEN equality_columns IS NOT NULL THEN N'EQUALITY: ' + equality_columns + N' ' - ELSE N'' - END + CASE WHEN inequality_columns IS NOT NULL THEN N'INEQUALITY: ' + inequality_columns + N' ' - ELSE N'' - END + CASE WHEN included_columns IS NOT NULL THEN N'INCLUDES: ' + included_columns + N' ' - ELSE N'' - END, - [create_tsql] AS N'CREATE INDEX [ix_' + table_name + N'_' - + REPLACE(REPLACE(REPLACE(REPLACE( - ISNULL(equality_columns,N'')+ - CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END - + ISNULL(inequality_columns,''),',','') - ,'[',''),']',''),' ','_') - + CASE WHEN included_columns IS NOT NULL THEN N'_includes' ELSE N'' END + N'] ON ' - + [statement] + N' (' + ISNULL(equality_columns,N'') - + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END - + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + - ') ' + CASE WHEN included_columns IS NOT NULL THEN N' INCLUDE (' + included_columns + N')' ELSE N'' END - + N' WITH (' - + N'FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - + N';' - , - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';' - ); - - CREATE TABLE #ForeignKeys ( - [database_id] INT NOT NULL, - [database_name] NVARCHAR(128) NOT NULL , - [schema_name] NVARCHAR(128) NOT NULL , - foreign_key_name NVARCHAR(256), - parent_object_id INT, - parent_object_name NVARCHAR(256), - referenced_object_id INT, - referenced_object_name NVARCHAR(256), - is_disabled BIT, - is_not_trusted BIT, - is_not_for_replication BIT, - parent_fk_columns NVARCHAR(MAX), - referenced_fk_columns NVARCHAR(MAX), - update_referential_action_desc NVARCHAR(16), - delete_referential_action_desc NVARCHAR(60) - ) - - CREATE TABLE #IndexCreateTsql ( - index_sanity_id INT NOT NULL, - create_tsql NVARCHAR(MAX) NOT NULL - ) - - CREATE TABLE #DatabaseList ( - DatabaseName NVARCHAR(256), - secondary_role_allow_connections_desc NVARCHAR(50) - - ) - - CREATE TABLE #PartitionCompressionInfo ( - [index_sanity_id] INT NULL, - [partition_compression_detail] VARCHAR(8000) NULL - ) - - CREATE TABLE #Statistics ( - database_id INT NOT NULL, - database_name NVARCHAR(256) NOT NULL, - table_name NVARCHAR(128) NULL, - schema_name NVARCHAR(128) NULL, - index_name NVARCHAR(128) NULL, - column_names NVARCHAR(4000) NULL, - statistics_name NVARCHAR(128) NULL, - last_statistics_update DATETIME NULL, - days_since_last_stats_update INT NULL, - rows BIGINT NULL, - rows_sampled BIGINT NULL, - percent_sampled DECIMAL(18, 1) NULL, - histogram_steps INT NULL, - modification_counter BIGINT NULL, - percent_modifications DECIMAL(18, 1) NULL, - modifications_before_auto_update INT 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 - ); - - CREATE TABLE #ComputedColumns - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NULL, - database_id INT NOT NULL, - table_name NVARCHAR(128) NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - column_name NVARCHAR(128) NULL, - is_nullable BIT NULL, - definition NVARCHAR(MAX) NULL, - uses_database_collation BIT NOT NULL, - is_persisted BIT NOT NULL, - is_computed BIT NOT NULL, - is_function INT NOT NULL, - column_definition NVARCHAR(MAX) NULL - ); - - CREATE TABLE #TraceStatus - ( - TraceFlag VARCHAR(10) , - status BIT , - Global BIT , - Session BIT - ); - - CREATE TABLE #TemporalTables - ( - index_sanity_id INT IDENTITY(1, 1) NOT NULL, - database_name NVARCHAR(128) NOT NULL, - database_id INT NOT NULL, - schema_name NVARCHAR(128) NOT NULL, - table_name NVARCHAR(128) NOT NULL, - history_table_name NVARCHAR(128) 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 - ); - -/* Sanitize our inputs */ -SELECT - @OutputServerName = QUOTENAME(@OutputServerName), - @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName) - - -IF @GetAllDatabases = 1 - BEGIN - INSERT INTO #DatabaseList (DatabaseName) - SELECT DB_NAME(database_id) - FROM sys.databases - WHERE user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE' - AND database_id > 4 - AND DB_NAME(database_id) NOT LIKE 'ReportServer%' - AND is_distributor = 0; - - /* Skip non-readable databases in an AG - see Github issue #1160 */ - IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') - BEGIN - SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( - SELECT d.name - FROM sys.dm_hadr_availability_replica_states rs - INNER JOIN sys.databases d ON rs.replica_id = d.replica_id - INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id - WHERE rs.role_desc = ''SECONDARY'' - AND r.secondary_role_allow_connections_desc = ''NO'');' - EXEC sp_executesql @dsql; - - IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'Skipped non-readable AG secondary databases.', - N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', - N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', - 'http://FirstResponderKit.org', '', '', '', '' - ); - END - END - - END -ELSE - BEGIN - INSERT INTO #DatabaseList - ( DatabaseName ) - SELECT CASE WHEN @DatabaseName IS NULL OR @DatabaseName = N'' THEN DB_NAME() - ELSE @DatabaseName END - END - -SET @NumDatabases = @@ROWCOUNT; - -/* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ - -BEGIN TRY - IF @NumDatabases >= 50 AND @BringThePain != 1 - BEGIN - - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', - N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary insanity for the server and/or user.', - N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', - 'http://FirstResponderKit.org', '', '', '', '' - ); - - - SELECT bir.blitz_result_id, - bir.check_id, - bir.index_sanity_id, - bir.Priority, - bir.findings_group, - bir.finding, - bir.database_name, - bir.URL, - bir.details, - bir.index_definition, - bir.secret_columns, - bir.index_usage_summary, - bir.index_size_summary, - bir.create_tsql, - bir.more_info - FROM #BlitzIndexResults AS bir - - RETURN; - - END -END TRY -BEGIN CATCH - RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; - - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; - END CATCH; - -/* Permission granted or unnecessary? Ok, let's go! */ - -DECLARE c1 CURSOR -LOCAL FAST_FORWARD -FOR -SELECT DatabaseName FROM #DatabaseList WHERE COALESCE(secondary_role_allow_connections_desc, 'OK') <> 'NO' ORDER BY DatabaseName - -OPEN c1 -FETCH NEXT FROM c1 INTO @DatabaseName - WHILE @@FETCH_STATUS = 0 -BEGIN - - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; - RAISERROR (@DatabaseName, 0, 1) WITH NOWAIT; - -SELECT @DatabaseID = [database_id] -FROM sys.databases - WHERE [name] = @DatabaseName - AND user_access_desc='MULTI_USER' - AND state_desc = 'ONLINE'; - -/* Last startup */ -SELECT @DaysUptime = CAST(DATEDIFF(hh,create_date,GETDATE())/24. AS NUMERIC (23,2)) -FROM sys.databases -WHERE database_id = 2; - -IF @DaysUptime = 0 SET @DaysUptime = .01; - ----------------------------------------- ---STEP 1: OBSERVE THE PATIENT ---This step puts index information into temp tables. ----------------------------------------- -BEGIN TRY - BEGIN - - --Validate SQL Server Verson - - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 - )) <= 9 - BEGIN - SET @msg=N'sp_BlitzIndex is only supported on SQL Server 2008 and higher. The version of this instance is: ' + @SQLServerProductVersion; - RAISERROR(@msg,16,1); - END - - --Short circuit here if database name does not exist. - IF @DatabaseName IS NULL OR @DatabaseID IS NULL - BEGIN - SET @msg='Database does not exist or is not online/multi-user: cannot proceed.' - RAISERROR(@msg,16,1); - END - - --Validate parameters. - IF (@Mode NOT IN (0,1,2,3,4)) - BEGIN - SET @msg=N'Invalid @Mode parameter. 0=diagnose, 1=summarize, 2=index detail, 3=missing index detail, 4=diagnose detail'; - RAISERROR(@msg,16,1); - END - - IF (@Mode <> 0 AND @TableName IS NOT NULL) - BEGIN - SET @msg=N'Setting the @Mode doesn''t change behavior if you supply @TableName. Use default @Mode=0 to see table detail.'; - RAISERROR(@msg,16,1); - END - - IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) - BEGIN - SET @msg=N'@Filter only appies when @Mode=0 and @TableName is not specified. Please try again.'; - RAISERROR(@msg,16,1); - END - - IF (@SchemaName IS NOT NULL AND @TableName IS NULL) - BEGIN - SET @msg='We can''t run against a whole schema! Specify a @TableName, or leave both NULL for diagnosis.' - RAISERROR(@msg,16,1); - END - - - IF (@TableName IS NOT NULL AND @SchemaName IS NULL) - BEGIN - SET @SchemaName=N'dbo' - SET @msg='@SchemaName wasn''t specified-- assuming schema=dbo.' - RAISERROR(@msg,1,1) WITH NOWAIT; - END - - --If a table is specified, grab the object id. - --Short circuit if it doesn't exist. - IF @TableName IS NOT NULL - BEGIN - SET @dsql = N' - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @ObjectID= OBJECT_ID - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on - so.schema_id=sc.schema_id - where so.type in (''U'', ''V'') - and so.name=' + QUOTENAME(@TableName,'''')+ N' - and sc.name=' + QUOTENAME(@SchemaName,'''')+ N' - /*Has a row in sys.indexes. This lets us get indexed views.*/ - and exists ( - SELECT si.name - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si - WHERE so.object_id=si.object_id) - OPTION (RECOMPILE);'; - - SET @params='@ObjectID INT OUTPUT' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - EXEC sp_executesql @dsql, @params, @ObjectID=@ObjectID OUTPUT; - - IF @ObjectID IS NULL - BEGIN - SET @msg=N'Oh, this is awkward. I can''t find the table or indexed view you''re looking for in that database.' + CHAR(10) + - N'Please check your parameters.' - RAISERROR(@msg,1,1); - RETURN; - END - END - - --set @collation - SELECT @collation=collation_name - FROM sys.databases - WHERE database_id=@DatabaseID; - - --insert columns for clustered indexes and heaps - --collect info on identity columns for this one - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', - CAST(ic.seed_value AS BIGINT), - CAST(ic.increment_value AS INT), - CAST(ic.last_value AS BIGINT), - ic.is_not_for_replication - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON - si.object_id=c.object_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.identity_columns ic ON - c.object_id=ic.object_id and - c.column_id=ic.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) - EXEC sp_executesql @dsql; - - --insert columns for nonclustered indexes - --this uses a full join to sys.index_columns - --We don't collect info on identity columns here. They may be in NC indexes, but we just analyze identities in the base table. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', - s.name, - si.object_id, - si.index_id, - sc.key_ordinal, - sc.is_included_column, - sc.is_descending_key, - sc.partition_ordinal, - c.name as column_name, - st.name as system_type_name, - c.max_length, - c.[precision], - c.[scale], - c.collation_name, - c.is_nullable, - c.is_identity, - c.is_computed, - c.is_replicated, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL AS is_sparse' END + N', - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL AS is_filestream' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON - si.object_id=c.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS sc ON - sc.object_id = si.object_id - and sc.index_id=si.index_id - AND sc.column_id=c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS st ON - c.system_type_id=st.system_type_id - AND c.user_type_id=st.user_type_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id - WHERE si.index_id not in (0,1) ' - + CASE WHEN @ObjectID IS NOT NULL - THEN N' AND si.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - ELSE N'' END - + N'OPTION (RECOMPILE);'; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream ) - EXEC sp_executesql @dsql; - - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - so.object_id, - si.index_id, - si.type, - ' + QUOTENAME(@DatabaseName, '''') + ' AS database_name, - COALESCE(sc.NAME, ''Unknown'') AS [schema_name], - COALESCE(so.name, ''Unknown'') AS [object_name], - COALESCE(si.name, ''Unknown'') AS [index_name], - CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, - si.is_unique, - si.is_primary_key, - CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, - 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, - si.is_disabled, - si.is_hypothetical, - si.is_padded, - si.fill_factor,' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN ' - CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition - ELSE '''' - END AS filter_definition' ELSE ''''' AS filter_definition' END + ' - , ISNULL(us.user_seeks, 0), ISNULL(us.user_scans, 0), - ISNULL(us.user_lookups, 0), ISNULL(us.user_updates, 0), us.last_user_seek, us.last_user_scan, - us.last_user_lookup, us.last_user_update, - so.create_date, so.modify_date - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si WITH (NOLOCK) - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas sc ON so.schema_id = sc.schema_id - 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)) + ' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + - CASE WHEN @TableName IS NOT NULL THEN ' and so.name=' + QUOTENAME(@TableName,'''') + ' ' ELSE '' END + - 'OPTION ( RECOMPILE ); - '; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; - 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_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, - is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, user_seeks, user_scans, - user_lookups, user_updates, last_user_seek, last_user_scan, last_user_lookup, last_user_update, - create_date, modify_date ) - EXEC sp_executesql @dsql; - - - RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; - IF @BringThePain = 0 AND @SkipPartitions = 0 - BEGIN - /* Count the total number of partitions */ - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; - EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; - IF @Rowcount > 100 - BEGIN - RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',16,1) WITH NOWAIT; - SET @SkipPartitions = 1; - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'Some Checks Were Skipped', - '@SkipPartitions Forced to 1', - 'http://FirstResponderKit.org', CAST(@Rowcount AS VARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' - ); - END - END - - - - IF (@SkipPartitions = 0) - BEGIN - IF (SELECT LEFT(@SQLServerProductVersion, - CHARINDEX('.',@SQLServerProductVersion,0)-1 )) <= 2147483647 --Make change here - BEGIN - - RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - - --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 - --This change was made because on a table with lots of paritions, the OUTER APPLY was crazy slow. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN 'par.data_compression_desc ' ELSE 'null as data_compression_desc' END + ' - FROM ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + ', NULL, NULL,NULL) AS os ON - ps.object_id=os.object_id and ps.index_id=os.index_id and ps.partition_number=os.partition_number - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + ' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END - ELSE - BEGIN - RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; - --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. - --If you have a lot of paritions and this suddenly starts running for a long time, change it back. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, - ps.object_id, - s.name, - ps.index_id, - ps.partition_number, - ps.row_count, - ps.reserved_page_count * 8. / 1024. AS reserved_MB, - ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, - ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id - AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ - AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os - WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); - '; - END; - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; - INSERT #IndexPartitionSanity ( [database_id], - [object_id], - [schema_name], - index_id, - partition_number, - row_count, - reserved_MB, - reserved_LOB_MB, - reserved_row_overflow_MB, - leaf_insert_count, - leaf_delete_count, - leaf_update_count, - range_scan_count, - singleton_lookup_count, - forwarded_fetch_count, - lob_fetch_in_pages, - lob_fetch_in_bytes, - row_overflow_fetch_in_pages, - row_overflow_fetch_in_bytes, - row_lock_count, - row_lock_wait_count, - row_lock_wait_in_ms, - page_lock_count, - page_lock_wait_count, - page_lock_wait_in_ms, - index_lock_promotion_attempt_count, - index_lock_promotion_count, - data_compression_desc ) - EXEC sp_executesql @dsql; - - END; --End Check For @SkipPartitions = 0 - - - - RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT id.database_id, id.object_id, ' + QUOTENAME(@DatabaseName,'''') + N', sc.[name], so.[name], id.statement , gs.avg_total_user_cost, - gs.avg_user_impact, gs.user_seeks, gs.user_scans, gs.unique_compiles,id.equality_columns, - id.inequality_columns,id.included_columns - FROM sys.dm_db_missing_index_groups ig - JOIN sys.dm_db_missing_index_details id ON ig.index_handle = id.index_handle - JOIN sys.dm_db_missing_index_group_stats gs ON ig.index_group_handle = gs.group_handle - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so on - id.object_id=so.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc on - so.schema_id=sc.schema_id - WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + ' - ' + CASE WHEN @ObjectID IS NULL THEN N'' - ELSE N'and id.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - END + - N'OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, - avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, - inequality_columns, included_columns) - EXEC sp_executesql @dsql; - - SET @dsql = N' - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' 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 - 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(''.'', ''varchar(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(''.'', ''varchar(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); - - RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; - INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, - is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, - [update_referential_action_desc], [delete_referential_action_desc] ) - EXEC sp_executesql @dsql; - - - IF @SkipStatistics = 0 - BEGIN - IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) - OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) - OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) - BEGIN - RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, ddsp.last_updated) AS last_statistics_update, - DATEDIFF(DAY, ddsp.last_updated, GETDATE()) AS days_since_last_stats_update, - ddsp.rows, - ddsp.rows_sampled, - CAST(ddsp.rows_sampled / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) AS percent_sampled, - ddsp.steps AS histogram_steps, - ddsp.modification_counter, - CASE WHEN ddsp.modification_counter > 0 - THEN CAST(ddsp.modification_counter / ( 1. * NULLIF(ddsp.rows, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE ddsp.modification_counter - END AS percent_modifications, - CASE WHEN ddsp.rows < 500 THEN 500 - ELSE CAST(( ddsp.rows * .20 ) + 500 AS INT) - 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 - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END - ELSE - BEGIN - RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - 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, - CONVERT(DATETIME, STATS_DATE(s.object_id, s.stats_id)) AS last_statistics_update, - DATEDIFF(DAY, STATS_DATE(s.object_id, s.stats_id), GETDATE()) AS days_since_last_stats_update, - si.rowcnt, - si.rowmodctr, - CASE WHEN si.rowmodctr > 0 THEN CAST(si.rowmodctr / ( 1. * NULLIF(si.rowcnt, 0) ) * 100 AS DECIMAL(18, 1)) - ELSE si.rowmodctr - END AS percent_modifications, - CASE WHEN si.rowcnt < 500 THEN 500 - ELSE CAST(( si.rowcnt * .20 ) + 500 AS INT) - 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, - ' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' - THEN N's.has_filter, - s.filter_definition' - ELSE N'NULL AS has_filter, - NULL AS filter_definition' END - + N' - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats AS s - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.sysindexes si - ON si.name = s.name - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj - ON s.object_id = obj.object_id - INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch - ON sch.schema_id = obj.schema_id - LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i - ON i.object_id = s.object_id - AND i.index_id = s.stats_id - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON sc.column_id = c.column_id AND sc.object_id = c.object_id - WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id - ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') - ) ca (column_names) - WHERE obj.is_ms_shipped = 0 - AND si.rowcnt > 0 - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) - - EXEC sp_executesql @dsql; - END - - END - - IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) - BEGIN - RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - t.name AS table_name, - s.name AS schema_name, - c.name AS column_name, - cc.is_nullable, - cc.definition, - cc.uses_database_collation, - cc.is_persisted, - cc.is_computed, - CASE WHEN cc.definition LIKE ''%.%'' THEN 1 ELSE 0 END AS is_function, - ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + - '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + - CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.computed_columns AS cc - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c - ON cc.object_id = c.object_id - AND cc.column_id = c.column_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON t.object_id = cc.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON s.schema_id = t.schema_id - OPTION (RECOMPILE);' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - INSERT #ComputedColumns - ( [database_name], database_id, table_name, schema_name, column_name, is_nullable, definition, - uses_database_collation, is_persisted, is_computed, is_function, column_definition ) - EXEC sp_executesql @dsql; - - END - - RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; - INSERT #TraceStatus - EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS') - - IF (PARSENAME(@SQLServerProductVersion, 4) >= 13) - BEGIN - RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], - s.name AS schema_name, - t.name AS table_name, - oa.hsn as history_schema_name, - oa.htn AS history_table_name, - c1.name AS start_column_name, - c2.name AS end_column_name, - p.name AS period_name - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.periods AS p - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t - ON p.object_id = t.object_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c1 - ON t.object_id = c1.object_id - AND p.start_column_id = c1.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c2 - ON t.object_id = c2.object_id - AND p.end_column_id = c2.column_id - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s - ON t.schema_id = s.schema_id - CROSS APPLY ( SELECT s2.name as hsn, t2.name htn - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t2 - INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s2 - ON t2.schema_id = s2.schema_id - WHERE t2.object_id = t.history_table_id - AND t2.temporal_type = 1 /*History table*/ ) AS oa - WHERE t.temporal_type IN ( 2, 4 ) /*BOL currently points to these types, but has no definition for 4*/ - OPTION (RECOMPILE); - ' - - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); - - INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_table_name, - history_schema_name, start_column_name, end_column_name, period_name ) - - EXEC sp_executesql @dsql; - - END - -END -END TRY -BEGIN CATCH - RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; - - IF @dsql IS NOT NULL - BEGIN - SET @msg= 'Last @dsql: ' + @dsql; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; - - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; -END CATCH; - FETCH NEXT FROM c1 INTO @DatabaseName -END -DEALLOCATE c1; - - - - - - ----------------------------------------- ---STEP 2: PREP THE TEMP TABLES ---EVERY QUERY AFTER THIS GOES AGAINST TEMP TABLES ONLY. ----------------------------------------- - -RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names = D1.key_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') ,TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D1 ( key_column_names ) - -RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET partition_key_column_name = D1.partition_key_column_name -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1,''))) D1 - ( partition_key_column_name ) - -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order ) - -RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key - WHEN 1 THEN N' DESC' - ELSE N'' - END AS col_definition - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 0 /*Just Keys*/ - AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ - ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D2 ( key_column_names_with_sort_order_no_types ) - -RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names = D3.include_column_names -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names ); - -RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET include_column_names_no_types = D3.include_column_names_no_types -FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - AND c.is_included_column = 1 /*Just includes*/ - ORDER BY c.column_name /*Order doesn't matter in includes, - this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) - ) D3 ( include_column_names_no_types ); - -RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; -UPDATE #IndexSanity -SET count_included_columns = D4.count_included_columns, - count_key_columns = D4.count_key_columns -FROM #IndexSanity si - CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 - ELSE 0 - END) AS count_included_columns, - SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 - ELSE 0 - END) AS count_key_columns - FROM #IndexColumns c - WHERE c.database_id= si.database_id - AND c.schema_name = si.schema_name - AND c.object_id = si.object_id - AND c.index_id = si.index_id - ) AS D4 ( count_included_columns, count_key_columns ); - -RAISERROR (N'Updating index_sanity_id on #IndexPartitionSanity',0,1) WITH NOWAIT; -UPDATE #IndexPartitionSanity -SET index_sanity_id = i.index_sanity_id -FROM #IndexPartitionSanity ps - JOIN #IndexSanity i ON ps.[object_id] = i.[object_id] - AND ps.index_id = i.index_id - AND i.database_id = ps.database_id - AND i.schema_name = ps.schema_name - - -RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; -INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], partition_count, total_rows, total_reserved_MB, - total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_range_scan_count, - total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, - total_forwarded_fetch_count,total_row_lock_count, - total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, - total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, - avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, - total_index_lock_promotion_count, data_compression_desc ) - SELECT index_sanity_id, ipp.database_id, ipp.schema_name, - COUNT(*), SUM(row_count), SUM(reserved_MB), SUM(reserved_LOB_MB), - SUM(reserved_row_overflow_MB), - SUM(range_scan_count), - SUM(singleton_lookup_count), - SUM(leaf_delete_count), - SUM(leaf_update_count), - SUM(forwarded_fetch_count), - SUM(row_lock_count), - SUM(row_lock_wait_count), - SUM(row_lock_wait_in_ms), - CASE WHEN SUM(row_lock_wait_in_ms) > 0 THEN - SUM(row_lock_wait_in_ms)/(1.*SUM(row_lock_wait_count)) - ELSE 0 END AS avg_row_lock_wait_in_ms, - SUM(page_lock_count), - SUM(page_lock_wait_count), - SUM(page_lock_wait_in_ms), - CASE WHEN SUM(page_lock_wait_in_ms) > 0 THEN - SUM(page_lock_wait_in_ms)/(1.*SUM(page_lock_wait_count)) - ELSE 0 END AS avg_page_lock_wait_in_ms, - SUM(index_lock_promotion_attempt_count), - SUM(index_lock_promotion_count), - LEFT(MAX(data_compression_info.data_compression_rollup),8000) - FROM #IndexPartitionSanity ipp - /* individual partitions can have distinct compression settings, just roll them into a list here*/ - OUTER APPLY (SELECT STUFF(( - SELECT N', ' + data_compression_desc - FROM #IndexPartitionSanity ipp2 - WHERE ipp.[object_id]=ipp2.[object_id] - AND ipp.[index_id]=ipp2.[index_id] - AND ipp.database_id = ipp2.database_id - AND ipp.schema_name = ipp2.schema_name - ORDER BY ipp2.partition_number - FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '')) - data_compression_info(data_compression_rollup) - GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name - ORDER BY index_sanity_id -OPTION ( RECOMPILE ); - -RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; -UPDATE #MissingIndexes -SET is_low = CASE WHEN (user_seeks + user_scans) < 10000 - OR avg_user_impact < 70. THEN 1 - ELSE 0 - END; - -RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; -UPDATE #IndexSanity - SET is_referenced_by_foreign_key=1 -FROM #IndexSanity s -JOIN #ForeignKeys fk ON - s.object_id=fk.referenced_object_id - AND s.database_id=fk.database_id - AND LEFT(s.key_column_names,LEN(fk.referenced_fk_columns)) = fk.referenced_fk_columns - -RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWAIT; -UPDATE nc -SET secret_columns= - N'[' + - CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS VARCHAR(10)) END + - CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + - CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + - CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + - /* Uniquifiers only needed on non-unique clustereds-- not heaps */ - CASE tb.is_unique WHEN 0 THEN ' [UNIQUIFIER]' ELSE N'' END - END - , count_secret_columns= - CASE tb.index_id WHEN 0 THEN 1 ELSE - tb.count_key_columns + - CASE tb.is_unique WHEN 0 THEN 1 ELSE 0 END - END -FROM #IndexSanity AS nc -JOIN #IndexSanity AS tb ON nc.object_id=tb.object_id - AND nc.database_id = tb.database_id - AND nc.schema_name = tb.schema_name - AND tb.index_id IN (0,1) -WHERE nc.index_id > 1; - -RAISERROR (N'Update index_secret on #IndexSanity for heaps and non-unique clustered.',0,1) WITH NOWAIT; -UPDATE tb -SET secret_columns= CASE tb.index_id WHEN 0 THEN '[RID]' ELSE '[UNIQUIFIER]' END - , count_secret_columns = 1 -FROM #IndexSanity AS tb -WHERE tb.index_id = 0 /*Heaps-- these have the RID */ - OR (tb.index_id=1 AND tb.is_unique=0); /* Non-unique CX: has uniquifer (when needed) */ - - -RAISERROR (N'Populate #IndexCreateTsql.',0,1) WITH NOWAIT; -INSERT #IndexCreateTsql (index_sanity_id, create_tsql) -SELECT - index_sanity_id, - ISNULL ( - /* Script drops for disabled non-clustered indexes*/ - CASE WHEN is_disabled = 1 AND index_id <> 1 - THEN N'--DROP INDEX ' + QUOTENAME([index_name]) + N' ON ' - + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) - ELSE - CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' - ELSE - CASE WHEN is_XML = 1 OR is_spatial=1 THEN N'' /* Not even trying for these just yet...*/ - ELSE - CASE WHEN is_primary_key=1 THEN - N'ALTER TABLE ' + QUOTENAME([schema_name]) + - N'.' + QUOTENAME([object_name]) + - N' ADD CONSTRAINT [' + - index_name + - N'] PRIMARY KEY ' + - CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + - key_column_names_with_sort_order_no_types + N' )' - WHEN is_CX_columnstore= 1 THEN - N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) - ELSE /*Else not a PK or cx columnstore */ - N'CREATE ' + - CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + - CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + - CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' - ELSE N'' END + - N'INDEX [' - + index_name + N'] ON ' + - QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + - CASE WHEN is_NC_columnstore=1 THEN - N' (' + ISNULL(include_column_names_no_types,'') + N' )' - ELSE /*Else not colunnstore */ - N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' - + CASE WHEN include_column_names_no_types IS NOT NULL THEN - N' INCLUDE (' + include_column_names_no_types + N')' - ELSE N'' - END - END /*End non-colunnstore case */ - + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END - END /*End Non-PK index CASE */ - + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN - N' WITH (' - + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' - + N'ONLINE=?, SORT_IN_TEMPDB=?' - + N')' - ELSE N'' END - + N';' - END /*End non-spatial and non-xml CASE */ - END - END, '[Unknown Error]') - AS create_tsql -FROM #IndexSanity; - -RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; -;WITH [maps] - AS ( SELECT - index_sanity_id, - partition_number, - data_compression_desc, - partition_number - ROW_NUMBER() OVER (PARTITION BY ips.index_sanity_id, data_compression_desc ORDER BY partition_number ) AS [rN] - FROM #IndexPartitionSanity ips - ), - [grps] - AS ( SELECT MIN([maps].[partition_number]) AS [MinKey] , - MAX([maps].[partition_number]) AS [MaxKey] , - index_sanity_id, - maps.data_compression_desc - FROM [maps] - GROUP BY [maps].[rN], index_sanity_id, maps.data_compression_desc) -INSERT #PartitionCompressionInfo - (index_sanity_id, partition_compression_detail) -SELECT DISTINCT grps.index_sanity_id , SUBSTRING(( STUFF((SELECT ', ' + ' Partition' - + CASE WHEN [grps2].[MinKey] < [grps2].[MaxKey] - THEN +'s ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' - ' - + CAST([grps2].[MaxKey] AS VARCHAR) - + ' use ' + grps2.data_compression_desc - ELSE ' ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' uses ' + grps2.data_compression_desc - END AS [Partitions] - FROM [grps] AS grps2 - WHERE grps2.index_sanity_id = grps.index_sanity_id - ORDER BY grps2.MinKey, grps2.MaxKey - FOR XML PATH('') , - TYPE - ).[value]('.', 'VARCHAR(MAX)'), 1, 1, '') ), 0, 8000) AS [partition_compression_detail] -FROM grps; - -RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; -UPDATE sz -SET sz.data_compression_desc = pci.partition_compression_detail -FROM #IndexSanitySize sz -JOIN #PartitionCompressionInfo AS pci -ON pci.index_sanity_id = sz.index_sanity_id; - - - -/*This is for debugging*/ ---SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; ---SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; ---SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; ---SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; ---SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; ---SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; ---SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults; ---SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; ---SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; ---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; -/*End debug*/ - - ----------------------------------------- ---STEP 3: DIAGNOSE THE PATIENT ----------------------------------------- - - -BEGIN TRY ----------------------------------------- ---If @TableName is specified, just return information for that table. ---The @Mode parameter doesn't matter if you're looking at a specific table. ----------------------------------------- -IF @TableName IS NOT NULL -BEGIN - RAISERROR(N'@TableName specified, giving detail only on that table.', 0,1) WITH NOWAIT; - - --We do a left join here in case this is a disabled NC. - --In that case, it won't have any size info/pages allocated. - - - WITH table_mode_cte AS ( - SELECT - s.db_schema_object_indexid, - s.key_column_names, - s.index_definition, - ISNULL(s.secret_columns,N'') AS secret_columns, - s.fill_factor, - s.index_usage_summary, - sz.index_op_stats, - ISNULL(sz.index_size_summary,'') /*disabled NCs will be null*/ AS index_size_summary, - partition_compression_detail , - ISNULL(sz.index_lock_wait_summary,'') AS index_lock_wait_summary, - s.is_referenced_by_foreign_key, - (SELECT COUNT(*) - FROM #ForeignKeys fk WHERE fk.parent_object_id=s.object_id - AND PATINDEX (fk.parent_fk_columns, s.key_column_names)=1) AS FKs_covered_by_index, - s.last_user_seek, - s.last_user_scan, - s.last_user_lookup, - s.last_user_update, - s.create_date, - s.modify_date, - ct.create_tsql, - 1 AS display_order - FROM #IndexSanity s - LEFT JOIN #IndexSanitySize sz ON - s.index_sanity_id=sz.index_sanity_id - LEFT JOIN #IndexCreateTsql ct ON - s.index_sanity_id=ct.index_sanity_id - LEFT JOIN #PartitionCompressionInfo pci ON - pci.index_sanity_id = s.index_sanity_id - WHERE s.[object_id]=@ObjectID - UNION ALL - SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + - N' (' + @ScriptVersionName + ')' , - N'SQL Server First Responder Kit' , - N'http://FirstResponderKit.org' , - N'From Your Community Volunteers', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - 0 AS display_order - ) - SELECT - db_schema_object_indexid AS [Details: db_schema.table.index(indexid)], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - secret_columns AS [Secret Columns], - fill_factor AS [Fillfactor], - index_usage_summary AS [Usage Stats], - index_op_stats AS [Op Stats], - index_size_summary AS [Size], - partition_compression_detail AS [Compression Type], - index_lock_wait_summary AS [Lock Waits], - is_referenced_by_foreign_key AS [Referenced by FK?], - FKs_covered_by_index AS [FK Covered by Index?], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Write], - create_date AS [Created], - modify_date AS [Last Modified], - create_tsql AS [Create TSQL] - FROM table_mode_cte - ORDER BY display_order ASC, key_column_names ASC - OPTION ( RECOMPILE ); - - IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL - BEGIN - - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT N'Missing index.' AS Finding , - N'http://BrentOzar.com/go/Indexaphobia' AS URL , - mi.[statement] + - ' Est. Benefit: ' - + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS [Estimated Benefit], - missing_index_details AS [Missing Index Request] , - index_estimated_impact AS [Estimated Impact], - create_tsql AS [Create TSQL] - FROM #MissingIndexes mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - WHERE mi.[object_id] = @ObjectID - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - AND (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - ORDER BY is_low, magic_benefit_number DESC - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No missing indexes.' AS finding; - - SELECT - column_name AS [Column Name], - (SELECT COUNT(*) - FROM #IndexColumns c2 - WHERE c2.column_name=c.column_name - AND c2.key_ordinal IS NOT NULL) - + CASE WHEN c.index_id = 1 AND c.key_ordinal IS NOT NULL THEN - -1+ (SELECT COUNT(DISTINCT index_id) - FROM #IndexColumns c3 - WHERE c3.index_id NOT IN (0,1)) - ELSE 0 END - AS [Found In], - system_type_name + - CASE max_length WHEN -1 THEN N' (max)' ELSE - CASE - WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' - WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' - ELSE '' - END - END - AS [Type], - CASE is_computed WHEN 1 THEN 'yes' ELSE '' END AS [Computed?], - max_length AS [Length (max bytes)], - [precision] AS [Prec], - [scale] AS [Scale], - CASE is_nullable WHEN 1 THEN 'yes' ELSE '' END AS [Nullable?], - CASE is_identity WHEN 1 THEN 'yes' ELSE '' END AS [Identity?], - CASE is_replicated WHEN 1 THEN 'yes' ELSE '' END AS [Replicated?], - CASE is_sparse WHEN 1 THEN 'yes' ELSE '' END AS [Sparse?], - CASE is_filestream WHEN 1 THEN 'yes' ELSE '' END AS [Filestream?], - collation_name AS [Collation] - FROM #IndexColumns AS c - WHERE index_id IN (0,1); - - IF (SELECT TOP 1 parent_object_id FROM #ForeignKeys) IS NOT NULL - BEGIN - SELECT [database_name] + N':' + parent_object_name + N': ' + foreign_key_name AS [Foreign Key], - parent_fk_columns AS [Foreign Key Columns], - referenced_object_name AS [Referenced Table], - referenced_fk_columns AS [Referenced Table Columns], - is_disabled AS [Is Disabled?], - is_not_trusted AS [Not Trusted?], - is_not_for_replication [Not for Replication?], - [update_referential_action_desc] AS [Cascading Updates?], - [delete_referential_action_desc] AS [Cascading Deletes?] - FROM #ForeignKeys - ORDER BY [Foreign Key] - OPTION ( RECOMPILE ); - END - ELSE - SELECT 'No foreign keys.' AS finding; -END - ---If @TableName is NOT specified... ---Act based on the @Mode and @Filter. (@Filter applies only when @Mode=0 "diagnose") -ELSE -BEGIN; - IF @Mode IN (0, 4) /* DIAGNOSE*/ - BEGIN; - RAISERROR(N'@Mode=0 or 4, we are diagnosing.', 0,1) WITH NOWAIT; - - ---------------------------------------- - --Multiple Index Personalities: Check_id 0-10 - ---------------------------------------- - BEGIN; - - --SELECT [object_id], key_column_names, database_id - -- FROM #IndexSanity - -- WHERE index_type IN (1,2) /* Clustered, NC only*/ - -- AND is_hypothetical = 0 - -- AND is_disabled = 0 - -- GROUP BY [object_id], key_column_names, database_id - -- HAVING COUNT(*) > 1 - - - RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; - WITH duplicate_indexes - AS ( SELECT [object_id], key_column_names, database_id, [schema_name] - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical = 0 - AND is_disabled = 0 - AND is_primary_key = 0 - GROUP BY [object_id], key_column_names, database_id, [schema_name] - HAVING COUNT(*) > 1) - 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 1 AS check_id, - ip.index_sanity_id, - 50 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Duplicate keys' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM duplicate_indexes di - JOIN #IndexSanity ip ON di.[object_id] = ip.[object_id] - AND ip.database_id = di.database_id - AND ip.[schema_name] = di.[schema_name] - AND di.key_column_names = ip.key_column_names - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id AND ip.database_id = ips.database_id - /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ - WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END - AND ip.is_primary_key = 0 - ORDER BY ip.object_id, ip.key_column_names_with_sort_order - OPTION ( RECOMPILE ); - - RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; - WITH borderline_duplicate_indexes - AS ( SELECT DISTINCT database_id, [object_id], first_key_column_name, key_column_names, - COUNT([object_id]) OVER ( PARTITION BY database_id, [object_id], first_key_column_name ) AS number_dupes - FROM #IndexSanity - WHERE index_type IN (1,2) /* Clustered, NC only*/ - AND is_hypothetical=0 - AND is_disabled=0 - AND is_primary_key = 0) - 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 2 AS check_id, - ip.index_sanity_id, - 60 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Borderline duplicate keys' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, - ip.db_schema_object_indexid AS details, - ip.index_definition, - ip.secret_columns, - ip.index_usage_summary, - ips.index_size_summary - FROM #IndexSanity AS ip - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id - WHERE EXISTS ( - SELECT di.[object_id] - FROM borderline_duplicate_indexes AS di - WHERE di.[object_id] = ip.[object_id] AND - di.database_id = ip.database_id AND - di.first_key_column_name = ip.first_key_column_name AND - di.key_column_names <> ip.key_column_names AND - di.number_dupes > 1 - ) - AND ip.is_primary_key = 0 - /* WHERE clause skips near-duplicate indexes when getting all databases or using PainRelief mode */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - ORDER BY ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names - OPTION ( RECOMPILE ); - - END - ---------------------------------------- - --Aggressive Indexes: Check_id 10-19 - ---------------------------------------- - BEGIN; - - RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page) with long average waits', 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 11 AS check_id, - i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with long average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) > 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 12: Total lock wait time > 5 minutes (row + page) with short average waits', 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 12 AS check_id, - i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with short average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) < 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id - OPTION ( RECOMPILE ); - - END - - ---------------------------------------- - --Index Hoarder: Check_id 20-29 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 20: >=7 NC indexes on any given table. Yes, 7 is an arbitrary number.', 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 20 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 100 AS Priority, - 'Index Hoarder' AS findings_group, - 'Many NC indexes on a single table' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, - i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, - '' AS secret_columns, - REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(total_reads) AS MONEY), 1), N'.00', N'') + N' reads (ALL); ' - + REPLACE(CONVERT(NVARCHAR(30),CAST(SUM(user_updates) AS MONEY), 1), N'.00', N'') + N' writes (ALL); ', - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(total_rows) AS MONEY), 1), N'.00', N'') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - GROUP BY db_schema_object_name, [i].[database_name] - HAVING COUNT(*) >= 7 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - IF @Filter = 1 /*@Filter=1 is "ignore unusued" */ - BEGIN - RAISERROR(N'Skipping checks on unused indexes (21 and 22) because @Filter=1', 0,1) WITH NOWAIT; - END - ELSE /*Otherwise, go ahead and do the checks*/ - BEGIN - RAISERROR(N'check_id 21: >=5 percent of indexes are unused. Yes, 5 is an arbitrary number.', 0,1) WITH NOWAIT; - DECLARE @percent_NC_indexes_unused NUMERIC(29,1); - DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); - - SELECT @percent_NC_indexes_unused =( 100.00 * SUM(CASE WHEN total_reads = 0 THEN 1 - ELSE 0 - END) ) / COUNT(*) , - @NC_indexes_unused_reserved_MB = SUM(CASE WHEN total_reads = 0 THEN sz.total_reserved_MB - ELSE 0 - END) - FROM #IndexSanity i - 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()) - 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, - secret_columns, index_usage_summary, index_size_summary ) - SELECT 21 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'More than 5 percent NC indexes are unused' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + - N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, - i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, - '' AS secret_columns, - CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' - + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, - - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND i.is_unique = 0 - AND total_reads = 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 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()) - GROUP BY i.database_name - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 22: NC indexes with 0 reads. (Borderline) and >= 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with High Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates >= 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); - END /*end checks only run when @Filter <> 1*/ - - RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Borderline: Wide indexes (7 or more columns)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' - + i.db_schema_object_indexid AS details, i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE ( count_key_columns + count_included_columns ) >= 7 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT database_id, [object_id], - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND key_ordinal > 0 - GROUP BY database_id, object_id - ) - 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 24 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide clustered index (> 3 columns OR > 16 bytes)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' - + CAST(cc.sum_max_length AS NVARCHAR(10)) - + N' bytes in clustered index:' + i.db_schema_object_name - + N'. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, - i.index_usage_summary, - ip.index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND i.database_id = cc.database_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (count_key_columns > 3 /*More than three key columns.*/ - OR cc.sum_max_length > 16 /*More than 16 bytes in key */) - AND i.is_CX_columnstore = 0 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 25: Addicted to nullable columns.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) AS non_nullable_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 25 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to nulls' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = ip.database_id - AND cc.[schema_name] = ip.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND cc.non_nullable_columns < 2 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE max_length WHEN -1 THEN 1 ELSE 0 END) AS count_lob_columns, - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 26 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide tables: 35+ cols or > 2000 non-LOB bytes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' has ' + CAST((total_columns) AS NVARCHAR(10)) - + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) - + N' bytes.' + - CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) AS NVARCHAR(10)) - + ' columns are LOB types.' ELSE '' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (cc.total_columns >= 35 OR - cc.sum_max_length >= 2000) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 27: Addicted to strings.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE WHEN system_type_name IN ('varchar','nvarchar','char') OR max_length=-1 THEN 1 ELSE 0 END) AS string_or_LOB_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 27 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to strings' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns. Check if data types are valid.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND calc1.non_string_or_lob_columns <= 1 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Non-Unique clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name - + N' and all NC indexes. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, - i.index_usage_summary, - ip.index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND is_unique=0 /* not unique */ - AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ) - - RAISERROR(N'check_id 29: NC indexes with 0 reads. (Borderline) and < 10,000 writes', 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 22 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with Low Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates < 10000 - 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 - ORDER BY i.db_schema_object_indexid - OPTION ( RECOMPILE ); - - END - ---------------------------------------- - --Feature-Phobic Indexes: Check_id 30-39 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; - /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 - */ - - SELECT database_name, - SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, - 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes - INTO #index_includes - FROM #IndexSanity - GROUP BY database_name; - - IF NOT (@Mode = 0) - 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 30 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - database_name AS [Database Name], - N'No indexes use includes' AS finding, 'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'No indexes use includes' AS details, - database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes = 0 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 0,1) WITH NOWAIT; - IF NOT (@Mode = 0) - 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 31 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: Includes are used in < 3% of indexes' AS findings, - database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, - N'Entire database' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 32: filtered indexes and indexed views', 0,1) WITH NOWAIT; - - IF NOT (@Mode = 0) - 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 DISTINCT - 32 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: No filtered indexes or indexed views exist' AS finding, - i.database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'These are NOT always needed-- but do you know when you would use them?' AS details, - i.database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #IndexSanity i - WHERE i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE filter_definition <> '' ) - AND i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE is_indexed_view = 1 ) - OPTION ( RECOMPILE ); - END; - - RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, - i.index_sanity_id AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Potential filtered index (based on column name)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexColumns ic - JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] - AND ic.database_id =i.database_id - AND ic.schema_name = i.schema_name - AND ic.[index_id]=i.[index_id] - AND i.[index_id] > 1 /* non-clustered index */ - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (column_name LIKE 'is%' - OR column_name LIKE '%archive%' - OR column_name LIKE '%active%' - OR column_name LIKE '%flag%') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - ---------------------------------------- - --Self Loathing Indexes : Check_id 40-49 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: nonclustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id > 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ - CASE WHEN (last_user_update IS NULL OR user_updates < 1) - THEN N'No writes have been made.' - ELSE - N'Last write was ' + CONVERT(NVARCHAR(16),last_user_update,121) + N' and ' + - CAST(user_updates AS NVARCHAR(25)) + N' updates have been made.' - END - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Hypothetical Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Hypothetical Index: ' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - N'' AS index_usage_summary, - N'' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_hypothetical = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; - --Note: disabled NC indexes will have O rows in #IndexSanitySize! - 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 42 AS check_id, - index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Disabled Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Disabled Index:' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - 'DISABLED' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_disabled = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 43: Heaps with forwarded records or deletes', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 43 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heaps with forwarded records or deletes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + ' forwarded fetches, ' - + CAST(h.leaf_delete_count AS NVARCHAR(256)) + ' deletes against heap:' - + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 44: Large Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 44 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Large Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 45 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Medium Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows >= 10000 AND sz.total_rows < 100000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; - WITH heaps_cte - AS ( SELECT [object_id], - [database_id], - [schema_name], - SUM(forwarded_fetch_count) AS forwarded_fetch_count, - SUM(leaf_delete_count) AS leaf_delete_count - FROM #IndexPartitionSanity - GROUP BY [object_id], - [database_id], - [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) - 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 46 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Small Active heap' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Should this table be a heap? ' + db_schema_object_indexid AS details, - i.index_definition, - 'N/A' AS secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity i - LEFT JOIN heaps_cte h ON i.[object_id] = h.[object_id] - AND i.[database_id] = h.[database_id] - AND i.[schema_name] = h.[schema_name] - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) - AND sz.total_rows < 10000 - AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 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 47 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heap with a Nonclustered Primary Key' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' 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 - WHERE i.index_type = 2 AND i.is_primary_key = 1 AND i.secret_columns LIKE '%RID%' - OPTION ( RECOMPILE ); - - END; - ---------------------------------------- - --Indexaphobia - --Missing indexes with value >= 5 million: : Check_id 50-59 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 50: Indexaphobia.', 0,1) WITH NOWAIT; - WITH index_size_cte - AS ( SELECT i.database_id, - i.schema_name, - i.[object_id], - MAX(i.index_sanity_id) AS index_sanity_id, - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days, - ISNULL ( - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) - AS NVARCHAR(30))+ N' NC indexes exist (' + - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 1024 - THEN CAST(CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END )/1024. - - AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB); ' - ELSE CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - AS NVARCHAR(30)) + N'MB); ' - END + - CASE WHEN MAX(sz.[total_rows]) >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(30),CAST(MAX(sz.[total_rows]) AS MONEY), 1), '.00', '') - END + - + N' Estimated Rows;' - ,N'') AS index_size_summary - FROM #IndexSanity AS i - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id AND i.database_id = sz.database_id - WHERE i.is_hypothetical = 0 - AND i.is_disabled = 0 - GROUP BY i.database_id, i.schema_name, i.[object_id]) - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, - index_usage_summary, index_size_summary, create_tsql, more_info ) - - SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], - index_estimated_impact, t.index_size_summary, create_tsql, more_info - FROM - ( - SELECT ROW_NUMBER() OVER (ORDER BY mi.is_low, magic_benefit_number DESC) AS rownum, - 50 AS check_id, - sz.index_sanity_id, - 10 AS Priority, - N'Indexaphobia' AS findings_group, - N'High value missing index' + CASE mi.is_low - WHEN 0 THEN N' with High Impact' - WHEN 1 THEN N' with Low Impact' - END - AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Indexaphobia' AS URL, - mi.[statement] + - N' Est. benefit per day: ' + - CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' - ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( - (magic_benefit_number/@DaysUptime) - AS BIGINT) AS MONEY), 1), '.00', '') - END AS details, - missing_index_details AS [definition], - index_estimated_impact, - sz.index_size_summary, - mi.create_tsql, - mi.more_info, - magic_benefit_number, - mi.is_low - FROM #MissingIndexes mi - LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id - AND mi.database_id = sz.database_id - AND mi.schema_name = sz.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) - OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 - ) AS t - WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END - ORDER BY t.is_low, magic_benefit_number DESC - - - END - ---------------------------------------- - --Abnormal Psychology : Check_id 60-79 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 60: XML indexes', 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 60 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'XML Indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - N'' AS index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_XML = 1 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 61: Columnstore indexes', 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 61 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - CASE WHEN i.is_NC_columnstore=1 - THEN N'NC Columnstore Index' - ELSE N'Clustered Columnstore Index' - END AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_NC_columnstore = 1 OR i.is_CX_columnstore=1 - OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 62: Spatial indexes', 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 62 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Spatial indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_spatial = 1 OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 63: Compressed indexes', 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 63 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Compressed indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 64: Partitioned', 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 64 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Partitioned indexes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NOT NULL OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 65: Non-Aligned Partitioned', 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 65 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Non-Aligned index on a partitioned table' AS finding, - i.[database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanity AS iParent ON - i.[object_id]=iParent.[object_id] - AND i.database_id = iParent.database_id - AND i.schema_name = iParent.schema_name - AND iParent.index_id IN (0,1) /* could be a partitioned heap or clustered table */ - AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NULL - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently created tables/indexes (1 week)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was created on ' + - CONVERT(NVARCHAR(16),i.create_date,121) + - N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently modified tables/indexes (2 days)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_indexid + N' was modified on ' + - CONVERT(NVARCHAR(16),i.modify_date,121) + - N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' - AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND /*Exclude recently created tables.*/ - i.create_date < DATEADD(dd,-7,GETDATE()) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; - -- Allowed Ranges: - --int -2,147,483,648 to 2,147,483,647 - --smallint -32,768 to 32,768 - --tinyint 0 to 255 - - 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 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column within ' + - CAST (calc1.percent_remaining AS NVARCHAR(256)) - + N' percent end of range' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - CROSS APPLY ( - SELECT CAST(CASE WHEN ic.increment_value >= 0 - THEN - CASE ic.system_type_name - WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE 999 - END - ELSE --ic.increment_value is negative - CASE ic.system_type_name - WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE -1 - END - END AS NUMERIC(5,1)) AS percent_remaining - ) AS calc1 - WHERE i.index_id IN (1,0) - AND calc1.percent_remaining <= 30 - UNION ALL - SELECT 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column using a negative seed or increment other than 1' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE i.index_id IN (1,0) - AND (ic.seed_value < 0 OR ic.increment_value <> 1) - ORDER BY finding, details DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND collation_name <> @collation - GROUP BY [object_id], - database_id, - schema_name - ) - 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 69 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Column collation does not match database collation' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name - + N' has ' + CAST(column_count AS NVARCHAR(20)) - + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END - + N' with a different collation than the db collation of ' - + @collation AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.schema_name = i.schema_name - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count, - SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY object_id, - database_id, - schema_name - ) - 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 70 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Replicated columns' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name - + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) - + N' out of ' + CAST(column_count AS NVARCHAR(20)) - + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END - + N' in one or more publications.' - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND i.database_id = cc.database_id - AND i.schema_name = cc.schema_name - WHERE i.index_id IN (1,0) - AND replicated_column_count > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 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, more_info ) - SELECT 71 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Cascading Updates or Deletes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - N'Foreign Key ' + foreign_key_name + - N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' - + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' - + N' has settings:' - + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END - + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END - AS details, - [fk].[database_name] - AS index_definition, - N'N/A' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary, - (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) - AS more_info - FROM #ForeignKeys fk - WHERE ([delete_referential_action_desc] <> N'NO_ACTION' - OR [update_referential_action_desc] <> N'NO_ACTION') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; - IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) - AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) - BEGIN - 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 72 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - 'Columnstore Indexes are being used in conjunction with trace flag 834. Visit the link to see why this can be a bad idea' AS finding, - [database_name] AS [Database Name], - N'https://support.microsoft.com/en-us/kb/3210239' AS URL, - i.db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - ISNULL(sz.index_size_summary,'') AS index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type IN (5,6) - OPTION ( RECOMPILE ) - END - - END - - ---------------------------------------- - --Workaholics: Check_id 80-89 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 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 ) - - --Workaholics according to index_usage_stats - --This isn't perfect: it mentions the number of scans present in a plan - --A "scan" isn't necessarily a full scan, but hey, we gotta do the best with what we've got. - --in the case of things like indexed views, the operator might be in the plan but never executed - SELECT TOP 5 - 80 AS check_id, - i.index_sanity_id AS index_sanity_id, - 200 AS Priority, - N'Workaholics' AS findings_group, - N'Scan-a-lots (index_usage_stats)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, - REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') - + N' scans against ' + i.db_schema_object_indexid - + N'. Latest scan: ' + ISNULL(CAST(i.last_user_scan AS NVARCHAR(128)),'?') + N'. ' - + N'ScanFactor=' + CAST(((i.user_scans * iss.total_reserved_MB)/1000000.) AS NVARCHAR(256)) AS details, - ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, - ISNULL(i.secret_columns,'') AS secret_columns, - i.index_usage_summary AS index_usage_summary, - iss.index_size_summary AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id - WHERE ISNULL(i.user_scans,0) > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.user_scans * iss.total_reserved_MB DESC; - - RAISERROR(N'check_id 81: Top recent accesses (op stats)', 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 ) - --Workaholics according to index_operational_stats - --This isn't perfect either: range_scan_count contains full scans, partial scans, even seeks in nested loop ops - --But this can help bubble up some most-accessed tables - SELECT TOP 5 - 81 AS check_id, - i.index_sanity_id AS index_sanity_id, - 200 AS Priority, - N'Workaholics' AS findings_group, - N'Top recent accesses (index_op_stats)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, - ISNULL(REPLACE( - CONVERT(NVARCHAR(50),CAST((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), - N'.00',N'') - + N' uses of ' + i.db_schema_object_indexid + N'. ' - + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_range_scan_count AS MONEY),1),N'.00',N'') + N' scans or seeks. ' - + REPLACE(CONVERT(NVARCHAR(50), CAST(iss.total_singleton_lookup_count AS MONEY), 1),N'.00',N'') + N' singleton lookups. ' - + N'OpStatsFactor=' + CAST(((((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB))/1000000.) AS VARCHAR(256)),'') AS details, - ISNULL(i.key_column_names_with_sort_order,'N/A') AS index_definition, - ISNULL(i.secret_columns,'') AS secret_columns, - i.index_usage_summary AS index_usage_summary, - iss.index_size_summary AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id - WHERE (ISNULL(iss.total_range_scan_count,0) > 0 OR ISNULL(iss.total_singleton_lookup_count,0) > 0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC; - - - END - - ---------------------------------------- - --Statistics Info: Check_id 90-99 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 90: Outdated 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 ) - SELECT 90 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Statistic Abandonment Issues', - s.database_name, - '' AS URL, - 'Statistics on this table were last updated ' + - CASE s.last_statistics_update WHEN NULL THEN N' NEVER ' - ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + - ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + - ' modifications in that time, which is ' + - CONVERT(NVARCHAR(100), s.percent_modifications) + - '% of the table.' - END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 7) - AND s.percent_modifications >= 10. - AND s.rows >= 10000 - - RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Antisocial Samples', - s.database_name, - '' AS URL, - 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows_sampled < 1. - AND s.rows >= 10000 - - 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 ) - SELECT 92 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Cyberphobic Samples', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.no_recompute = 1 - - RAISERROR(N'check_id 93: Statistics with filters', 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 93 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Filter Fixation', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is filtered on [' + s.filter_definition + ']. It could be part of a filtered index, or just a filtered statistic. This is purely informational.' 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_filter = 1 - - END - - ---------------------------------------- - --Computed Column Info: Check_id 99-109 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 99: Computed Columns 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 ) - SELECT 99 AS check_id, - 50 AS Priority, - 'Cold Calculators' AS findings_group, - 'Serial Forcer' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition - + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_function = 1 - - RAISERROR(N'check_id 100: Computed Columns that are not Persisted.', 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 100 AS check_id, - 200 AS Priority, - 'Cold Calculators' AS findings_group, - 'Definition Defeatists' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is not persisted, which means it will be calculated when a query runs.' + - 'You can change this with the following command, if the definition is deterministic: ALTER TABLE ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' ALTER COLUMN ' + cc.column_name + - ' ADD PERSISTED' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_persisted = 0 - - ---------------------------------------- - --Temporal Table Info: Check_id 110-119 - ---------------------------------------- - RAISERROR(N'check_id 110: Temporal Tables.', 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 110 AS check_id, - 200 AS Priority, - 'Temporal Tables' AS findings_group, - 'Obsessive Compulsive Tables', - t.database_name, - '' AS URL, - 'The table ' + QUOTENAME(t.schema_name) + '.' + QUOTENAME(t.table_name) + ' is a temporal table, with rows versioned in ' - + QUOTENAME(t.history_schema_name) + '.' + QUOTENAME(t.history_table_name) + ' on History columns ' + QUOTENAME(t.start_column_name) + ' and ' + QUOTENAME(t.end_column_name) + '.' - AS details, - '' AS index_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #TemporalTables AS t - - - - END - - RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; - IF DATEDIFF(MM, @VersionDate, GETDATE()) > 6 - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - 'Outdated sp_BlitzIndex', 'sp_BlitzIndex is Over 6 Months Old', 'http://FirstResponderKit.org/', - 'Fine wine gets better with age, but this ' + @ScriptVersionName + ' is more like bad cheese. Time to get a new one.', - N'',N'',N'' - ); - END - - IF EXISTS(SELECT * FROM #BlitzIndexResults) - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' - ); - END - ELSE IF @Mode = 0 OR (@GetAllDatabases = 1 AND @Mode <> 4) - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'No Major Problems Found', - 'Nice Work!', - 'http://FirstResponderKit.org', 'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', 'The new default Mode 0 only looks for very serious index issues.', '', '' - ); - - END - ELSE - BEGIN - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' - ); - INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, - index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - 'No Problems Found', - 'Nice job! Or more likely, you have a nearly empty database.', - 'http://FirstResponderKit.org', 'Time to go read some blog posts.', '', '', '' - ); - - END - - RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; - - /*Return results.*/ - IF (@Mode = 0) - BEGIN - - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - WHERE br.check_id IN (0, 1, 11, 22, 43, 68, 50, 60, 61, 62, 63, 64, 65, 72) - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END - ELSE IF (@Mode = 4) - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END; /* End @Mode=0 or 4 (diagnose)*/ - ELSE IF @Mode=1 /*Summarize*/ - BEGIN - --This mode is to give some overall stats on the database. - RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; - - SELECT DB_NAME(i.database_id) AS [Database Name], - CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], - CAST(CAST(SUM(sz.total_reserved_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], - CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], - CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], - CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], - CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], - SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN - CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) - ELSE 0 END AS [ratio table: NC Indexes], - SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], - CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Heaps GB], - SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], - SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], - CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], - SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], - SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], - MAX(total_rows) AS [Max Row Count], - CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max Table GB], - CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], - MIN(create_date) AS [Oldest Create Date], - MAX(create_date) AS [Most Recent Create Date], - MAX(modify_date) AS [Most Recent Modify Date], - 1 AS [Display Order] - FROM #IndexSanity AS i - --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz - ON i.index_sanity_id=sz.index_sanity_id - GROUP BY DB_NAME(i.database_id) - UNION ALL - SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,0 AS display_order - ORDER BY [Display Order] ASC - OPTION (RECOMPILE); - - END /* End @Mode=1 (summarize)*/ - ELSE IF @Mode=2 /*Index Detail*/ - BEGIN - --This mode just spits out all the detail without filters. - --This supports slicing AND dicing in Excel - RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; - - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT - DECLARE @ValidOutputLocation BIT - DECLARE @LinkedServerDBCheck NVARCHAR(2000) - DECLARE @ValidLinkedServerDB INT - DECLARE @tmpdbchk table (cnt int) - DECLARE @StringToExecute NVARCHAR(MAX); - - IF @OutputServerName IS NOT NULL - BEGIN - IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END - ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) - BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')' - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk) - IF (@ValidLinkedServerDB > 0) - BEGIN - SET @ValidOutputServer = 1 - SET @ValidOutputLocation = 1 - END - ELSE - RAISERROR('The specified database was not found on the output server', 16, 0) - END - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0) - END - END - ELSE - BEGIN - IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - EXEC(@StringToExecute); - - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - SET @OutputDatabaseName = '[tempdb]'; - SET @OutputSchemaName = '[dbo]'; - SET @ValidOutputLocation = 1; - END - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1 - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - END - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0) - END - ELSE - BEGIN - SET @ValidOutputLocation = 0 - END - END - - /* @OutputTableName lets us export the results to a permanent table */ - DECLARE @RunID UNIQUEIDENTIFIER; - SET @RunID = NEWID(); - - IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) - BEGIN - DECLARE @TableExists BIT; - DECLARE @SchemaExists BIT; - SET @StringToExecute = - N'SET @SchemaExists = 0; - SET @TableExists = 0; - IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - SET @SchemaExists = 1 - IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 1'; - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT - - IF @SchemaExists = 1 - BEGIN - IF @TableExists = 0 - BEGIN - SET @StringToExecute = - N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ - ( - [id] INT IDENTITY(1,1) NOT NULL, - [run_id] UNIQUEIDENTIFIER, - [run_datetime] DATETIME, - [server_name] NVARCHAR(128), - [database_name] NVARCHAR(128), - [schema_name] NVARCHAR(128), - [table_name] NVARCHAR(128), - [index_name] NVARCHAR(128), - [index_id] INT, - [db_schema_object_indexid] NVARCHAR(500), - [object_type] NVARCHAR(15), - [index_definition] NVARCHAR(4000), - [key_column_names_with_sort_order] NVARCHAR(MAX), - [count_key_columns] INT, - [include_column_names] NVARCHAR(MAX), - [count_included_columns] INT, - [secret_columns] NVARCHAR(MAX), - [count_secret_columns] INT, - [partition_key_column_name] NVARCHAR(MAX), - [filter_definition] NVARCHAR(MAX), - [is_indexed_view] BIT, - [is_primary_key] BIT, - [is_XML] BIT, - [is_spatial] BIT, - [is_NC_columnstore] BIT, - [is_CX_columnstore] BIT, - [is_disabled] BIT, - [is_hypothetical] BIT, - [is_padded] BIT, - [fill_factor] INT, - [is_referenced_by_foreign_key] BIT, - [last_user_seek] DATETIME, - [last_user_scan] DATETIME, - [last_user_lookup] DATETIME, - [last_user_update] DATETIME, - [total_reads] BIGINT, - [user_updates] BIGINT, - [reads_per_write] MONEY, - [index_usage_summary] NVARCHAR(200), - [partition_count] INT, - [total_rows] BIGINT, - [total_reserved_MB] NUMERIC(29,2), - [total_reserved_LOB_MB] NUMERIC(29,2), - [total_reserved_row_overflow_MB] NUMERIC(29,2), - [index_size_summary] NVARCHAR(300), - [total_row_lock_count] BIGINT, - [total_row_lock_wait_count] BIGINT, - [total_row_lock_wait_in_ms] BIGINT, - [avg_row_lock_wait_in_ms] BIGINT, - [total_page_lock_count] BIGINT, - [total_page_lock_wait_count] BIGINT, - [total_page_lock_wait_in_ms] BIGINT, - [avg_page_lock_wait_in_ms] BIGINT, - [total_index_lock_promotion_attempt_count] BIGINT, - [total_index_lock_promotion_count] BIGINT, - [data_compression_desc] VARCHAR(8000), - [create_date] DATETIME, - [modify_date] DATETIME, - [more_info] NVARCHAR(500), - [display_order] INT, - CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) - );' - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) - - IF @ValidOutputServer = 1 - BEGIN - SET @StringToExecute = REPLACE(@StringToExecute,'''','''''') - EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END - ELSE - BEGIN - EXEC(@StringToExecute); - END - END /* @TableExists = 0 */ - - SET @StringToExecute = - N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 0 - ELSE - SET @TableExists = 1'; - - SET @TableExists = NULL - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT', @TableExists OUTPUT - - IF @TableExists = 1 - BEGIN - SET @StringToExecute = - N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ - ( - [run_id], - [run_datetime], - [server_name], - [database_name], - [schema_name], - [table_name], - [index_name], - [index_id], - [db_schema_object_indexid], - [object_type], - [index_definition], - [key_column_names_with_sort_order], - [count_key_columns], - [include_column_names], - [count_included_columns], - [secret_columns], - [count_secret_columns], - [partition_key_column_name], - [filter_definition], - [is_indexed_view], - [is_primary_key], - [is_XML], - [is_spatial], - [is_NC_columnstore], - [is_CX_columnstore], - [is_disabled], - [is_hypothetical], - [is_padded], - [fill_factor], - [is_referenced_by_foreign_key], - [last_user_seek], - [last_user_scan], - [last_user_lookup], - [last_user_update], - [total_reads], - [user_updates], - [reads_per_write], - [index_usage_summary], - [partition_count], - [total_rows], - [total_reserved_MB], - [total_reserved_LOB_MB], - [total_reserved_row_overflow_MB], - [index_size_summary], - [total_row_lock_count], - [total_row_lock_wait_count], - [total_row_lock_wait_in_ms], - [avg_row_lock_wait_in_ms], - [total_page_lock_count], - [total_page_lock_wait_count], - [total_page_lock_wait_in_ms], - [avg_page_lock_wait_in_ms], - [total_index_lock_promotion_attempt_count], - [total_index_lock_promotion_count], - [data_compression_desc], - [create_date], - [modify_date], - [more_info], - [display_order] - ) - SELECT ''@@@RunID@@@'', - ''@@@GETDATE@@@'', - ''@@@LocalServerName@@@'', - -- Below should be a copy/paste of the real query - -- Make sure all quotes are escaped - i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '''') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN ''TABLE'' - ELSE ''NonClustered'' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '''') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '''') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'''') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '''') AS [Partition Key Column Name], - ISNULL(filter_definition, '''') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE);'; - - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()) - SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) - EXEC(@StringToExecute); - END /* @TableExists = 1 */ - ELSE - RAISERROR('Creation of the output table failed.', 16, 0) - END /* @TableExists = 0 */ - ELSE - RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0) - END /* @ValidOutputLocation = 1 */ - ELSE - - - SELECT i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' - ELSE 'NonClustered' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], - ISNULL(filter_definition, '') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE); - - - - END /* End @Mode=2 (index detail)*/ - ELSE IF @Mode=3 /*Missing index Detail*/ - BEGIN - - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT - mi.database_name AS [Database Name], - mi.[schema_name] AS [Schema], - mi.table_name AS [Table], - CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) - AS [Magic Benefit Number], - mi.missing_index_details AS [Missing Index Details], - mi.avg_total_user_cost AS [Avg Query Cost], - mi.avg_user_impact AS [Est Index Improvement], - mi.user_seeks AS [Seeks], - mi.user_scans AS [Scans], - mi.unique_compiles AS [Compiles], - mi.equality_columns AS [Equality Columns], - mi.inequality_columns AS [Inequality Columns], - mi.included_columns AS [Included Columns], - mi.index_estimated_impact AS [Estimated Impact], - mi.create_tsql AS [Create TSQL], - mi.more_info AS [More Info], - 1 AS [Display Order], - mi.is_low - FROM #MissingIndexes AS mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - UNION ALL - SELECT - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - 100000000000, - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL, 0 AS [Display Order], NULL AS is_low - ORDER BY [Display Order] ASC, is_low, [Magic Benefit Number] DESC - OPTION (RECOMPILE); - - END /* End @Mode=3 (index detail)*/ -END -END TRY - -BEGIN CATCH - RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; - - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); - - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); - - WHILE @@trancount > 0 - ROLLBACK; - - RETURN; - END CATCH; -GO -IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); -GO - -ALTER PROCEDURE dbo.sp_BlitzLock -( - @Top INT = 2147483647, - @DatabaseName NVARCHAR(256) = NULL, - @StartDate DATETIME = '19000101', - @EndDate DATETIME = '99991231', - @ObjectName NVARCHAR(1000) = NULL, - @StoredProcName NVARCHAR(1000) = NULL, - @AppName NVARCHAR(256) = NULL, - @HostName NVARCHAR(256) = NULL, - @LoginName NVARCHAR(256) = NULL, - @EventSessionPath VARCHAR(256) = 'system_health*.xel', - @Debug BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -) -AS -BEGIN - -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version VARCHAR(30); -SET @Version = '1.0'; -SET @VersionDate = '20171201'; - - - IF @Help = 1 PRINT ' - /* - sp_BlitzLock from http://FirstResponderKit.org - - This script checks for and analyzes deadlocks from the system health session or a custom extended event path - - Variables 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. - - @ObjectName: If you want to filter to a specific able. - The object name has to be fully qualified ''Database.Schema.Table'' - - @StoredProcName: If you want to search for a single stored proc - The proc name has to be fully qualified ''Database.Schema.Sproc'' - - @AppName: If you want to filter to a specific application - - @HostName: If you want to filter to a specific host - - @LoginName: If you want to filter to a specific login - - @EventSessionPath: If you want to point this at an XE session rather than the system health session. - - - - 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. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - MIT License - - All other copyright for sp_BlitzLock are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - */'; - - - DECLARE @ProductVersion NVARCHAR(128); - DECLARE @ProductVersionMajor FLOAT; - DECLARE @ProductVersionMinor INT; - - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); - - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); - - - IF @ProductVersionMajor < 11.0 - BEGIN - RAISERROR( - 'sp_BlitzLock will throw a bunch of angry errors on versions of SQL Server earlier than 2012.', - 0, - 1) WITH NOWAIT; - RETURN; - END; - - IF @Top IS NULL - SET @Top = 2147483647; - - IF @StartDate IS NULL - SET @StartDate = '19000101'; - - IF @EndDate IS NULL - SET @EndDate = '99991231'; - - - IF OBJECT_ID('tempdb..#deadlock_data') IS NOT NULL - DROP TABLE #deadlock_data; - - IF OBJECT_ID('tempdb..#deadlock_process') IS NOT NULL - DROP TABLE #deadlock_process; - - IF OBJECT_ID('tempdb..#deadlock_stack') IS NOT NULL - DROP TABLE #deadlock_stack; - - IF OBJECT_ID('tempdb..#deadlock_resource') IS NOT NULL - DROP TABLE #deadlock_resource; - - IF OBJECT_ID('tempdb..#deadlock_owner_waiter') IS NOT NULL - DROP TABLE #deadlock_owner_waiter; - - IF OBJECT_ID('tempdb..#deadlock_findings') IS NOT NULL - DROP TABLE #deadlock_findings; - - CREATE TABLE #deadlock_findings - ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - check_id INT NOT NULL, - database_name NVARCHAR(256), - object_name NVARCHAR(1000), - finding_group NVARCHAR(100), - finding NVARCHAR(4000) - ); - - - /*Grab the initial set of XML to parse*/ - WITH xml - AS ( SELECT CONVERT(XML, event_data) AS deadlock_xml - FROM sys.fn_xe_file_target_read_file(@EventSessionPath, NULL, NULL, NULL) ) - SELECT TOP ( @Top ) xml.deadlock_xml - INTO #deadlock_data - FROM xml - WHERE xml.deadlock_xml.value('(/event/@name)[1]', 'VARCHAR(256)') = 'xml_deadlock_report' - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') >= @StartDate - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') < @EndDate - ORDER BY xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') - OPTION ( RECOMPILE ); - - - - /*Parse process and input buffer XML*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.value('@id', 'NVARCHAR(256)') AS id, - ca.dp.value('@currentdb', 'BIGINT') AS database_id, - ca.dp.value('@logused', 'BIGINT') AS log_used, - ca.dp.value('@waitresource', 'NVARCHAR(256)') AS wait_resource, - ca.dp.value('@waittime', 'BIGINT') AS wait_time, - ca.dp.value('@transactionname', 'NVARCHAR(256)') AS transaction_name, - ca.dp.value('@lasttranstarted', 'DATETIME2(7)') AS last_tran_started, - ca.dp.value('@lastbatchstarted', 'DATETIME2(7)') AS last_batch_started, - ca.dp.value('@lastbatchcompleted', 'DATETIME2(7)') AS last_batch_completed, - ca.dp.value('@lockMode', 'NVARCHAR(256)') AS lock_mode, - ca.dp.value('@trancount', 'BIGINT') AS transaction_count, - ca.dp.value('@clientapp', 'NVARCHAR(256)') AS client_app, - ca.dp.value('@hostname', 'NVARCHAR(256)') AS host_name, - ca.dp.value('@loginname', 'NVARCHAR(256)') AS login_name, - ca.dp.value('@isolationlevel', 'NVARCHAR(256)') AS isolation_level, - ca2.ib.query('.') AS input_buffer, - ca.dp.query('.') AS process_xml - INTO #deadlock_process - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process/inputbuf') AS ca2(ib) - WHERE (ca.dp.value('@currentdb', 'BIGINT') = DB_ID(@DatabaseName) OR @DatabaseName IS NULL) - AND (ca.dp.value('@clientapp', 'NVARCHAR(256)') = @AppName OR @AppName IS NULL) - AND (ca.dp.value('@hostname', 'NVARCHAR(256)') = @HostName OR @HostName IS NULL) - AND (ca.dp.value('@loginname', 'NVARCHAR(256)') = @LoginName OR @LoginName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Parse execution stack XML*/ - SELECT dp.id, - dp.event_date, - ca.dp.value('@procname', 'NVARCHAR(1000)') AS proc_name, - ca.dp.value('@sqlhandle', 'NVARCHAR(128)') AS sql_handle - INTO #deadlock_stack - FROM #deadlock_process AS dp - CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) - WHERE (ca.dp.value('@procname', 'NVARCHAR(256)') = @StoredProcName OR @StoredProcName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Grab the full resource list*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.query('.') AS resource_xml - INTO #deadlock_resource - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) - OPTION ( RECOMPILE ); - - - /*This parses object locks*/ - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(1000)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - INTO #deadlock_owner_waiter - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - WHERE (ca.dr.value('@objectname', 'NVARCHAR(1000)') = @ObjectName OR @ObjectName IS NULL) - OPTION ( RECOMPILE ); - - - - /*This parses page locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses key locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses rid locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) - CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) - CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - /*Get rid of nonsense*/ - DELETE dow - FROM #deadlock_owner_waiter AS dow - WHERE dow.owner_id = dow.waiter_id; - - /*Add some nonsense*/ - ALTER TABLE #deadlock_process - ADD waiter_mode NVARCHAR(256), - owner_mode NVARCHAR(256), - is_victim AS CONVERT(BIT, CASE WHEN id = victim_id THEN 1 ELSE 0 END); - - /*Update some nonsense*/ - UPDATE dp - SET dp.owner_mode = dow.owner_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 0; - - UPDATE dp - SET dp.waiter_mode = dow.waiter_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.victim_id = dow.waiter_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 1; - - - /*Begin checks based on parsed values*/ - - /*Check 1 is deadlocks by database*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 1 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Total database locks' AS finding_group, - 'This database had ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) - + ' deadlocks.' - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - /*Check 2 is deadlocks by object*/ - - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 2 AS check_id, - DB_NAME(dow.database_id) AS database_name, - dow.object_name AS object_name, - 'Total object deadlocks' AS finding_group, - 'This object was involved in ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.object_name)) - + ' deadlock(s).' - FROM #deadlock_owner_waiter AS dow - GROUP BY DB_NAME(dow.database_id), dow.object_name - OPTION ( RECOMPILE ); - - - /*Check 3 looks for Serializable locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 3 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Serializable locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of serializable deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'serializable%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 4 looks for Repeatable Read locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 4 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Repeatable Read locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of repeatable read deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'repeatable read%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 5 breaks down app, host, and login information*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 5 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Login, App, and Host locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) + - ' instances of deadlocks involving the login ' + - ISNULL(dp.login_name, 'UNKNOWN') + - ' from the application ' + - ISNULL(dp.client_app, 'UNKNOWN') + - ' on host ' + - ISNULL(dp.host_name, 'UNKNOWN') - AS finding - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id), dp.login_name, dp.client_app, dp.host_name - OPTION ( RECOMPILE ); - - - /*Check 6 breaks down the types of locks (object, page, key, etc.)*/ - WITH lock_types AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - dow.object_name, - SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) -1) AS lock, - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.id)) AS lock_count - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - GROUP BY DB_NAME(dp.database_id), SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) - 1), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 6 AS check_id, - lt.database_name, - lt.object_name, - 'Types of locks by object' AS finding_group, - 'This object has had ' + - STUFF((SELECT DISTINCT N', ' + lt2.lock_count + ' ' + 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'') - + ' locks' - FROM lock_types AS lt - OPTION ( RECOMPILE ); - - - /*Check 7 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name, - '''' + STUFF((SELECT DISTINCT N',' + ds2.sql_handle - FROM #deadlock_stack AS ds2 - WHERE ds2.id = ds.id - AND ds2.event_date = ds.event_date - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + '''' AS sql_handle_csv - FROM #deadlock_stack AS ds - GROUP BY PARSENAME(ds.proc_name, 3), - PARSENAME(ds.proc_name, 2), - PARSENAME(ds.proc_name, 1), - ds.id, - ds.proc_name, - ds.event_date - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzCache ' + - CASE WHEN ds.proc_name = 'adhoc' - THEN ' @OnlySqlHandles = ' + sql_handle_csv - ELSE '@StoredProcName = ' + - QUOTENAME(ds.proc_only_name, '''') - END + - ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - OPTION ( RECOMPILE ); - - IF @ProductVersionMajor >= 13 - BEGIN - - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.sql_handle, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name - FROM #deadlock_stack AS ds - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzQueryStore ' - + '@DatabaseName = ' - + QUOTENAME(ds.database_name, '''') - + ', ' - + '@StoredProcName = ' - + QUOTENAME(ds.proc_only_name, '''') - + ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - WHERE ds.proc_name <> 'adhoc' - OPTION ( RECOMPILE ); - END; - - - /*Check 8 gives you stored proc deadlock counts*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 8 AS check_id, - DB_NAME(dp.database_id) AS database_name, - ds.proc_name, - 'Stored Procedure Deadlocks', - 'The stored procedure ' - + PARSENAME(ds.proc_name, 2) - + '.' - + PARSENAME(ds.proc_name, 1) - + ' has been involved in ' - + CONVERT(NVARCHAR(10), COUNT_BIG(DISTINCT ds.id)) - + ' deadlocks.' - FROM #deadlock_stack AS ds - JOIN #deadlock_process AS dp - ON dp.id = ds.id - AND ds.event_date = dp.event_date - WHERE ds.proc_name <> 'adhoc' - GROUP BY DB_NAME(dp.database_id), ds.proc_name - OPTION(RECOMPILE); - - - /*Check 9 gives you more info queries for sp_BlitzIndex */ - WITH bi AS ( - SELECT DISTINCT - dow.object_name, - PARSENAME(dow.object_name, 3) AS database_name, - PARSENAME(dow.object_name, 2) AS schema_name, - PARSENAME(dow.object_name, 1) AS table_name - FROM #deadlock_owner_waiter AS dow - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 9 AS check_id, - bi.database_name, - bi.schema_name + '.' + bi.table_name, - 'More Info - Table' AS finding_group, - 'EXEC sp_BlitzIndex ' + - '@DatabaseName = ' + QUOTENAME(bi.database_name, '''') + - ', @SchemaName = ' + QUOTENAME(bi.schema_name, '''') + - ', @TableName = ' + QUOTENAME(bi.table_name, '''') + - ';' AS finding - FROM bi - OPTION ( RECOMPILE ); - - /*Check 10 gets total deadlock wait time per object*/ - WITH chopsuey AS ( - SELECT DISTINCT - PARSENAME(dow.object_name, 3) AS database_name, - dow.object_name, - CONVERT(VARCHAR(10), (SUM(DISTINCT dp.wait_time) / 1000) / 86400) AS wait_days, - CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT dp.wait_time) / 1000), 0), 108) AS wait_time_hms - FROM #deadlock_owner_waiter AS dow - JOIN #deadlock_process AS dp - ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - GROUP BY PARSENAME(dow.object_name, 3), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 10 AS check_id, - cs.database_name, - cs.object_name, - 'Total object deadlock wait time' AS finding_group, - 'This object has had ' - + CONVERT(VARCHAR(10), cs.wait_days) - + ':' + CONVERT(VARCHAR(20), cs.wait_time_hms, 108) - + ' [d/h/m/s] of deadlock wait time.' AS finding - FROM chopsuey AS cs - WHERE cs.object_name IS NOT NULL - OPTION ( RECOMPILE ); - - /*Check 11 gets total deadlock wait time per database*/ - WITH wait_time AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - SUM(CONVERT(BIGINT, dp.wait_time)) AS total_wait_time_ms - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 11 AS check_id, - wt.database_name, - '-' AS object_name, - 'Total database deadlock wait time' AS finding_group, - 'This database has had ' - + CONVERT(VARCHAR(10), (SUM(DISTINCT wt.total_wait_time_ms) / 1000) / 86400) - + ':' + CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT wt.total_wait_time_ms) / 1000), 0), 108) - + ' [d/h/m/s] of deadlock wait time.' - FROM wait_time AS wt - GROUP BY wt.database_name - OPTION ( RECOMPILE ); - - - /*Thank you goodnight*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - VALUES ( -1, - N'sp_BlitzLock ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - N'SQL Server First Responder Kit', - N'http://FirstResponderKit.org/', - N'To get help or add your own contributions, join us at http://FirstResponderKit.org.'); - - - - - /*Results*/ - WITH deadlocks - AS ( SELECT dp.event_date, - dp.id, - dp.victim_id, - dp.database_id, - dp.log_used, - dp.wait_resource, - CONVERT( - XML, - STUFF(( SELECT DISTINCT NCHAR(10) - + N' ' - + ISNULL(c.object_name, N'') - + N' ' AS object_name - FROM #deadlock_owner_waiter AS c - WHERE (dp.id = c.owner_id - OR dp.victim_id = c.waiter_id) - AND dp.event_date = c.event_date - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), - 1, 1, N'')) AS object_names, - dp.wait_time, - dp.transaction_name, - dp.last_tran_started, - dp.last_batch_started, - dp.last_batch_completed, - dp.lock_mode, - dp.transaction_count, - dp.client_app, - dp.host_name, - dp.login_name, - dp.isolation_level, - dp.process_xml.value('(//process/inputbuf/text())[1]', 'NVARCHAR(MAX)') AS inputbuf, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date ) AS dn, - DENSE_RANK() OVER ( ORDER BY dp.event_date ) AS en, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date ORDER BY dp.event_date ) -1 AS qn, - dp.is_victim, - ISNULL(dp.owner_mode, '-') AS owner_mode, - ISNULL(dp.waiter_mode, '-') AS waiter_mode - FROM #deadlock_process AS dp ) - SELECT d.event_date, - DB_NAME(d.database_id) AS database_name, - 'Deadlock #' - + CONVERT(NVARCHAR(10), d.en) - + ', Query #' - + CASE WHEN d.qn = 0 THEN N'1' ELSE CONVERT(NVARCHAR(10), d.qn) END - + CASE WHEN d.is_victim = 1 THEN ' - VICTIM' ELSE '' END - AS deadlock_group, - CONVERT(XML, N'' + d.inputbuf + N'') AS query, - d.object_names, - d.isolation_level, - d.owner_mode, - d.waiter_mode, - d.transaction_count, - d.login_name, - d.host_name, - d.client_app, - d.wait_time, - d.log_used, - d.last_tran_started, - d.last_batch_started, - d.last_batch_completed, - d.transaction_name - FROM deadlocks AS d - WHERE d.dn = 1 - ORDER BY d.event_date, is_victim DESC; - - - - SELECT df.check_id, df.database_name, df.object_name, df.finding_group, df.finding - FROM #deadlock_findings AS df - ORDER BY df.check_id - OPTION ( RECOMPILE ); - - - IF @Debug = 1 - BEGIN - - SELECT '#deadlock_data' AS table_name, * - FROM #deadlock_data AS dd - OPTION ( RECOMPILE ); - - SELECT '#deadlock_resource' AS table_name, * - FROM #deadlock_resource AS dr - OPTION ( RECOMPILE ); - - SELECT '#deadlock_owner_waiter' AS table_name, * - FROM #deadlock_owner_waiter AS dow - OPTION ( RECOMPILE ); - - SELECT '#deadlock_process' AS table_name, * - FROM #deadlock_process AS dp - OPTION ( RECOMPILE ); - - SELECT '#deadlock_stack' AS table_name, * - FROM #deadlock_stack AS ds - OPTION ( RECOMPILE ); - - END; -- End debug - - END; --Final End - -GO - -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 STATISTICS IO OFF; -SET STATISTICS TIME OFF; -GO - -DECLARE @msg NVARCHAR(MAX) = N''; - - -- Must be a compatible, on-prem version of SQL (2016+) -IF ( (SELECT SERVERPROPERTY ('EDITION')) <> 'SQL Azure' - AND (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) < 13 - ) - -- or Azure Database (not Azure Data Warehouse), running at database compat level 130+ -OR ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' - AND (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 - AND (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 - ) -BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on versions of SQL prior to 2016, or Azure Database compatibility < 130.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; -END; - -IF OBJECT_ID('dbo.sp_BlitzQueryStore') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzQueryStore AS RETURN 0;'); -GO - -ALTER PROCEDURE dbo.sp_BlitzQueryStore - @Help BIT = 0, - @DatabaseName NVARCHAR(128) = NULL , - @Top INT = 3, - @StartDate DATETIME2 = NULL, - @EndDate DATETIME2 = NULL, - @MinimumExecutionCount INT = NULL, - @DurationFilter DECIMAL(38,4) = NULL , - @StoredProcName NVARCHAR(128) = NULL, - @Failed BIT = 0, - @PlanIdFilter INT = NULL, - @QueryIdFilter INT = NULL, - @ExportToExcel BIT = 0, - @HideSummary BIT = 0 , - @SkipXML BIT = 0, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -WITH RECOMPILE -AS -BEGIN /*First BEGIN*/ - -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - -DECLARE @Version NVARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - -DECLARE /*Variables for the variable Gods*/ - @msg NVARCHAR(MAX) = N'', --Used to format RAISERROR messages in some places - @sql_select NVARCHAR(MAX) = N'', --Used to hold SELECT statements for dynamic SQL - @sql_where NVARCHAR(MAX) = N'', -- Used to hold WHERE clause for dynamic SQL - @duration_filter_ms DECIMAL(38,4) = (@DurationFilter * 1000.), --We accept Duration in seconds, but we filter in milliseconds (this is grandfathered from sp_BlitzCache) - @execution_threshold INT = 1000, --Threshold at which we consider a query to be frequently executed - @ctp_threshold_pct TINYINT = 10, --Percentage of CTFP at which we consider a query to be near parallel - @long_running_query_warning_seconds BIGINT = 300 * 1000 ,--Number of seconds (converted to milliseconds) at which a query is considered long running - @memory_grant_warning_percent INT = 10,--Percent of memory grant used compared to what's granted; used to trigger unused memory grant warning - @ctp INT,--Holds the CTFP value for the server - @min_memory_per_query INT,--Holds the server configuration value for min memory per query - @cr NVARCHAR(1) = NCHAR(13),--Special character - @lf NVARCHAR(1) = NCHAR(10),--Special character - @tab NVARCHAR(1) = NCHAR(9),--Special character - @error_severity INT,--Holds error info for try/catch blocks - @error_state INT,--Holds error info for try/catch blocks - @sp_params NVARCHAR(MAX) = N'@sp_Top INT, @sp_StartDate DATETIME2, @sp_EndDate DATETIME2, @sp_MinimumExecutionCount INT, @sp_MinDuration INT, @sp_StoredProcName NVARCHAR(128), @sp_PlanIdFilter INT, @sp_QueryIdFilter INT',--Holds parameters used in dynamic SQL - @is_azure_db BIT = 0, --Are we using Azure? I'm not. You might be. That's cool. - @compatibility_level TINYINT = 0, --Some functionality (T-SQL) isn't available in lower compat levels. We can use this to weed out those issues as we go. - @log_size_mb DECIMAL(38,2) = 0, - @avg_tempdb_data_file DECIMAL(38,2) = 0; - -/*Grabs CTFP setting*/ -SELECT @ctp = NULLIF(CAST(value AS INT), 0) -FROM sys.configurations -WHERE name = N'cost threshold for parallelism' -OPTION (RECOMPILE); - -/*Grabs min query memory setting*/ -SELECT @min_memory_per_query = CONVERT(INT, c.value) -FROM sys.configurations AS c -WHERE c.name = N'min memory per query (KB)' -OPTION (RECOMPILE); - -/*Grabs log size for datbase*/ -SELECT @log_size_mb = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID(@DatabaseName) -AND mf.type_desc = 'LOG'; - -/*Grab avg tempdb file size*/ -SELECT @avg_tempdb_data_file = AVG(((mf.size * 8) / 1024.)) -FROM sys.master_files AS mf -WHERE mf.database_id = DB_ID('tempdb') -AND mf.type_desc = 'ROWS'; - - -/*Help section*/ - -IF @Help = 1 - BEGIN - - SELECT N'You have requested assistance. It will arrive as soon as humanly possible.' AS [Take four red capsules, help is on the way]; - - PRINT N' - sp_BlitzQueryStore from http://FirstResponderKit.org - - This script displays your most resource-intensive queries from the Query Store, - and points to ways you can tune these queries to make them faster. - - - 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. - - Known limitations of this version: - - This query will not run on SQL Server versions less than 2016. - - This query will not run on Azure Databases with compatibility less than 130. - - This query will not run on Azure Data Warehouse. - - Unknown limitations of this version: - - Could be tickling - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - - - MIT License - - Copyright (c) 2016 Brent Ozar Unlimited - - 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. - '; - RETURN; - -END; - -/*Making sure your version is copasetic*/ -IF ( (SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' ) - BEGIN - SET @is_azure_db = 1; - - IF ( (SELECT SERVERPROPERTY ('ENGINEEDITION')) <> 5 - OR (SELECT [compatibility_level] FROM sys.databases WHERE [name] = DB_NAME()) < 130 - ) - BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on Azure Data Warehouse, or Azure Databases with DB compatibility < 130.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; - END; -ELSE IF ( (SELECT PARSENAME(CONVERT(NVARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4) ) < 13 ) - BEGIN - SELECT @msg = N'Sorry, sp_BlitzQueryStore doesn''t work on versions of SQL prior to 2016.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; - -/*Making sure at least one database uses QS*/ -IF ( SELECT COUNT(*) - FROM sys.databases AS d - WHERE d.is_query_store_on = 1 - AND d.user_access_desc='MULTI_USER' - AND d.state_desc = 'ONLINE' - AND d.name NOT IN ('master', 'model', 'msdb', 'tempdb', '32767') - AND d.is_distributor = 0 ) = 0 - BEGIN - SELECT @msg = N'You don''t currently have any databases with Query Store enabled.' + REPLICATE(CHAR(13), 7933); - PRINT @msg; - RETURN; - END; - -/*Making sure your databases are using QDS.*/ -RAISERROR('Checking database validity', 0, 1) WITH NOWAIT; - -IF (@is_azure_db = 1) - SET @DatabaseName = DB_NAME(); -ELSE -BEGIN - - /*If we're on Azure we don't need to check all this @DatabaseName stuff...*/ - - SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)); - - /*Did you set @DatabaseName?*/ - RAISERROR('Making sure [%s] isn''t NULL', 0, 1, @DatabaseName) WITH NOWAIT; - IF (@DatabaseName IS NULL) - BEGIN - RAISERROR('@DatabaseName cannot be NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - - /*Does the database exist?*/ - RAISERROR('Making sure [%s] exists', 0, 1, @DatabaseName) WITH NOWAIT; - IF ((DB_ID(@DatabaseName)) IS NULL) - BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) does not exist. Please check the name and try again.', 0, 1, @DatabaseName) WITH NOWAIT; - RETURN; - END; - - /*Is it online?*/ - RAISERROR('Making sure [%s] is online', 0, 1, @DatabaseName) WITH NOWAIT; - IF (DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' - BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) is not readable. Please check the name and try again. Better yet, check your server.', 0, 1, @DatabaseName); - RETURN; - END; -END; - -/*Does it have Query Store enabled?*/ -RAISERROR('Making sure [%s] has Query Store enabled', 0, 1, @DatabaseName) WITH NOWAIT; -IF - ((DB_ID(@DatabaseName)) IS NOT NULL AND @DatabaseName <> '') -AND - ( SELECT DB_NAME(d.database_id) - FROM sys.databases AS d - WHERE d.is_query_store_on = 1 - AND d.user_access_desc='MULTI_USER' - AND d.state_desc = 'ONLINE' - AND DB_NAME(d.database_id) = @DatabaseName ) IS NULL -BEGIN - RAISERROR('The @DatabaseName you specified ([%s]) does not have the Query Store enabled. Please check the name or settings, and try again.', 0, 1, @DatabaseName) WITH NOWAIT; - RETURN; -END; - -/*Check database compat level*/ - -RAISERROR('Checking database compatibility level', 0, 1) WITH NOWAIT; - -SELECT @compatibility_level = d.compatibility_level -FROM sys.databases AS d -WHERE d.name = @DatabaseName; - -RAISERROR('The @DatabaseName you specified ([%s])is running in compatibility level ([%d]).', 0, 1, @DatabaseName, @compatibility_level) WITH NOWAIT; - - -/*Making sure top is set to something if NULL*/ -IF ( @Top IS NULL ) - BEGIN - SET @Top = 3; - END; - -/* -This section determines if you have the Query Store wait stats DMV -*/ - -RAISERROR('Checking for query_store_wait_stats', 0, 1) WITH NOWAIT; - -DECLARE @ws_out INT, - @waitstats BIT, - @ws_sql NVARCHAR(MAX) = N'SELECT @i_out = COUNT(*) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.all_objects WHERE name = ''query_store_wait_stats'' OPTION (RECOMPILE);', - @ws_params NVARCHAR(MAX) = N'@i_out INT OUTPUT'; - -EXEC sys.sp_executesql @ws_sql, @ws_params, @i_out = @ws_out OUTPUT; - -SELECT @waitstats = CASE @ws_out WHEN 0 THEN 0 ELSE 1 END; - -SET @msg = N'Wait stats DMV ' + CASE @waitstats - WHEN 0 THEN N' does not exist, skipping.' - WHEN 1 THEN N' exists, will analyze.' - END; -RAISERROR(@msg, 0, 1) WITH NOWAIT; - -/* -This section determines if you have some additional columns present in 2017, in case they get back ported. -*/ - -RAISERROR('Checking for new columns in query_store_runtime_stats', 0, 1) WITH NOWAIT; - -DECLARE @nc_out INT, - @new_columns BIT, - @nc_sql NVARCHAR(MAX) = N'SELECT @i_out = COUNT(*) - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.all_columns AS ac - WHERE OBJECT_NAME(object_id) = ''query_store_runtime_stats'' - AND ac.name IN ( - ''avg_num_physical_io_reads'', - ''last_num_physical_io_reads'', - ''min_num_physical_io_reads'', - ''max_num_physical_io_reads'', - ''avg_log_bytes_used'', - ''last_log_bytes_used'', - ''min_log_bytes_used'', - ''max_log_bytes_used'', - ''avg_tempdb_space_used'', - ''last_tempdb_space_used'', - ''min_tempdb_space_used'', - ''max_tempdb_space_used'' - ) OPTION (RECOMPILE);', - @nc_params NVARCHAR(MAX) = N'@i_out INT OUTPUT'; - -EXEC sys.sp_executesql @nc_sql, @ws_params, @i_out = @nc_out OUTPUT; - -SELECT @new_columns = CASE @nc_out WHEN 12 THEN 1 ELSE 0 END; - -SET @msg = N'New query_store_runtime_stats columns ' + CASE @new_columns - WHEN 0 THEN N' do not exist, skipping.' - WHEN 1 THEN N' exist, will analyze.' - END; -RAISERROR(@msg, 0, 1) WITH NOWAIT; - - -/* -These are the temp tables we use -*/ - - -/* -This one holds the grouped data that helps use figure out which periods to examine -*/ - -RAISERROR(N'Creating temp tables', 0, 1) WITH NOWAIT; - -DROP TABLE IF EXISTS #grouped_interval; - -CREATE TABLE #grouped_interval -( - flat_date DATE NULL, - start_range DATETIME NULL, - end_range DATETIME NULL, - total_avg_duration_ms DECIMAL(38, 2) NULL, - total_avg_cpu_time_ms DECIMAL(38, 2) NULL, - total_avg_logical_io_reads_mb DECIMAL(38, 2) NULL, - total_avg_physical_io_reads_mb DECIMAL(38, 2) NULL, - total_avg_logical_io_writes_mb DECIMAL(38, 2) NULL, - total_avg_query_max_used_memory_mb DECIMAL(38, 2) NULL, - total_rowcount DECIMAL(38, 2) NULL, - total_count_executions BIGINT NULL, - total_avg_log_bytes_mb DECIMAL(38, 2) NULL, - total_avg_tempdb_space DECIMAL(38, 2) NULL, - INDEX gi_ix_dates CLUSTERED (start_range, end_range) -); - - -/* -These are the plans we focus on based on what we find in the grouped intervals -*/ -DROP TABLE IF EXISTS #working_plans; - -CREATE TABLE #working_plans -( - plan_id BIGINT, - query_id BIGINT, - pattern NVARCHAR(256), - INDEX wp_ix_ids CLUSTERED (plan_id, query_id) -); - - -/* -These are the gathered metrics we get from query store to generate some warnings and help you find your worst offenders -*/ -DROP TABLE IF EXISTS #working_metrics; - -CREATE TABLE #working_metrics -( - database_name NVARCHAR(256), - plan_id BIGINT, - query_id BIGINT, - /*these columns are from query_store_query*/ - proc_or_function_name NVARCHAR(256), - batch_sql_handle VARBINARY(64), - query_hash BINARY(8), - query_parameterization_type_desc NVARCHAR(256), - parameter_sniffing_symptoms NVARCHAR(4000), - count_compiles BIGINT, - avg_compile_duration DECIMAL(38,2), - last_compile_duration DECIMAL(38,2), - avg_bind_duration DECIMAL(38,2), - last_bind_duration DECIMAL(38,2), - avg_bind_cpu_time DECIMAL(38,2), - last_bind_cpu_time DECIMAL(38,2), - avg_optimize_duration DECIMAL(38,2), - last_optimize_duration DECIMAL(38,2), - avg_optimize_cpu_time DECIMAL(38,2), - last_optimize_cpu_time DECIMAL(38,2), - avg_compile_memory_kb DECIMAL(38,2), - last_compile_memory_kb DECIMAL(38,2), - /*These come from query_store_runtime_stats*/ - execution_type_desc NVARCHAR(128), - first_execution_time DATETIME2, - last_execution_time DATETIME2, - count_executions BIGINT, - avg_duration DECIMAL(38,2) , - last_duration DECIMAL(38,2), - min_duration DECIMAL(38,2), - max_duration DECIMAL(38,2), - avg_cpu_time DECIMAL(38,2), - last_cpu_time DECIMAL(38,2), - min_cpu_time DECIMAL(38,2), - max_cpu_time DECIMAL(38,2), - avg_logical_io_reads DECIMAL(38,2), - last_logical_io_reads DECIMAL(38,2), - min_logical_io_reads DECIMAL(38,2), - max_logical_io_reads DECIMAL(38,2), - avg_logical_io_writes DECIMAL(38,2), - last_logical_io_writes DECIMAL(38,2), - min_logical_io_writes DECIMAL(38,2), - max_logical_io_writes DECIMAL(38,2), - avg_physical_io_reads DECIMAL(38,2), - last_physical_io_reads DECIMAL(38,2), - min_physical_io_reads DECIMAL(38,2), - max_physical_io_reads DECIMAL(38,2), - avg_clr_time DECIMAL(38,2), - last_clr_time DECIMAL(38,2), - min_clr_time DECIMAL(38,2), - max_clr_time DECIMAL(38,2), - avg_dop BIGINT, - last_dop BIGINT, - min_dop BIGINT, - max_dop BIGINT, - avg_query_max_used_memory DECIMAL(38,2), - last_query_max_used_memory DECIMAL(38,2), - min_query_max_used_memory DECIMAL(38,2), - max_query_max_used_memory DECIMAL(38,2), - avg_rowcount DECIMAL(38,2), - last_rowcount DECIMAL(38,2), - min_rowcount DECIMAL(38,2), - max_rowcount DECIMAL(38,2), - /*These are 2017 only, AFAIK*/ - avg_num_physical_io_reads DECIMAL(38,2), - last_num_physical_io_reads DECIMAL(38,2), - min_num_physical_io_reads DECIMAL(38,2), - max_num_physical_io_reads DECIMAL(38,2), - avg_log_bytes_used DECIMAL(38,2), - last_log_bytes_used DECIMAL(38,2), - min_log_bytes_used DECIMAL(38,2), - max_log_bytes_used DECIMAL(38,2), - avg_tempdb_space_used DECIMAL(38,2), - last_tempdb_space_used DECIMAL(38,2), - min_tempdb_space_used DECIMAL(38,2), - max_tempdb_space_used DECIMAL(38,2), - /*These are computed columns to make some stuff easier down the line*/ - total_compile_duration AS avg_compile_duration * count_compiles, - total_bind_duration AS avg_bind_duration * count_compiles, - total_bind_cpu_time AS avg_bind_cpu_time * count_compiles, - total_optimize_duration AS avg_optimize_duration * count_compiles, - total_optimize_cpu_time AS avg_optimize_cpu_time * count_compiles, - total_compile_memory_kb AS avg_compile_memory_kb * count_compiles, - total_duration AS avg_duration * count_executions, - total_cpu_time AS avg_cpu_time * count_executions, - total_logical_io_reads AS avg_logical_io_reads * count_executions, - total_logical_io_writes AS avg_logical_io_writes * count_executions, - total_physical_io_reads AS avg_physical_io_reads * count_executions, - total_clr_time AS avg_clr_time * count_executions, - total_query_max_used_memory AS avg_query_max_used_memory * count_executions, - total_rowcount AS avg_rowcount * count_executions, - total_num_physical_io_reads AS avg_num_physical_io_reads * count_executions, - total_log_bytes_used AS avg_log_bytes_used * count_executions, - total_tempdb_space_used AS avg_tempdb_space_used * count_executions, - xpm AS NULLIF(count_executions, 0) / NULLIF(DATEDIFF(MINUTE, first_execution_time, last_execution_time), 0), - INDEX wm_ix_ids CLUSTERED (plan_id, query_id, query_hash) -); - - -/* -This is where we store some additional metrics, along with the query plan and text -*/ -DROP TABLE IF EXISTS #working_plan_text; - -CREATE TABLE #working_plan_text -( - database_name NVARCHAR(256), - plan_id BIGINT, - query_id BIGINT, - /*These are from query_store_plan*/ - plan_group_id BIGINT, - engine_version NVARCHAR(64), - compatibility_level INT, - query_plan_hash BINARY(8), - query_plan_xml XML, - is_online_index_plan BIT, - is_trivial_plan BIT, - is_parallel_plan BIT, - is_forced_plan BIT, - is_natively_compiled BIT, - force_failure_count BIGINT, - last_force_failure_reason_desc NVARCHAR(256), - count_compiles BIGINT, - initial_compile_start_time DATETIME2, - last_compile_start_time DATETIME2, - last_execution_time DATETIME2, - avg_compile_duration DECIMAL(38,2), - last_compile_duration BIGINT, - min_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - max_used_grant_kb DECIMAL(38,2), --This column is updated from dm_exec_query_stats when sql_handle for query exists there - percent_memory_grant_used AS CONVERT(MONEY, ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100.), - /*These are from query_store_query*/ - query_sql_text NVARCHAR(MAX), - statement_sql_handle VARBINARY(64), - is_part_of_encrypted_module BIT, - has_restricted_text BIT, - /*This is from query_context_settings*/ - context_settings NVARCHAR(512), - /*This is from #working_plans*/ - pattern NVARCHAR(512), - top_three_waits NVARCHAR(MAX), - INDEX wpt_ix_ids CLUSTERED (plan_id, query_id, query_plan_hash) -); - - -/* -This is where we store warnings that we generate from the XML and metrics -*/ -DROP TABLE IF EXISTS #working_warnings; - -CREATE TABLE #working_warnings -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_or_function_name NVARCHAR(256), - plan_multiple_plans BIT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - query_cost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - is_trivial BIT, - trace_flags_session NVARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name NVARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - is_slow_plan BIT, - is_compile_more BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_big_log BIT, - is_big_tempdb BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - warnings NVARCHAR(4000) - INDEX ww_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); - - -DROP TABLE IF EXISTS #working_wait_stats; - -CREATE TABLE #working_wait_stats -( - plan_id BIGINT, - wait_category TINYINT, - wait_category_desc NVARCHAR(256), - total_query_wait_time_ms BIGINT, - avg_query_wait_time_ms DECIMAL(38, 2), - last_query_wait_time_ms BIGINT, - min_query_wait_time_ms BIGINT, - max_query_wait_time_ms BIGINT, - wait_category_mapped AS CASE wait_category - WHEN 0 THEN N'UNKNOWN' - WHEN 1 THEN N'SOS_SCHEDULER_YIELD' - WHEN 2 THEN N'THREADPOOL' - WHEN 3 THEN N'LCK_M_%' - WHEN 4 THEN N'LATCH_%' - WHEN 5 THEN N'PAGELATCH_%' - WHEN 6 THEN N'PAGEIOLATCH_%' - WHEN 7 THEN N'RESOURCE_SEMAPHORE_QUERY_COMPILE' - WHEN 8 THEN N'CLR%, SQLCLR%' - WHEN 9 THEN N'DBMIRROR%' - WHEN 10 THEN N'XACT%, DTC%, TRAN_MARKLATCH_%, MSQL_XACT_%, TRANSACTION_MUTEX' - WHEN 11 THEN N'SLEEP_%, LAZYWRITER_SLEEP, SQLTRACE_BUFFER_FLUSH, SQLTRACE_INCREMENTAL_FLUSH_SLEEP, SQLTRACE_WAIT_ENTRIES, FT_IFTS_SCHEDULER_IDLE_WAIT, XE_DISPATCHER_WAIT, REQUEST_FOR_DEADLOCK_SEARCH, LOGMGR_QUEUE, ONDEMAND_TASK_QUEUE, CHECKPOINT_QUEUE, XE_TIMER_EVENT' - WHEN 12 THEN N'PREEMPTIVE_%' - WHEN 13 THEN N'BROKER_% (but not BROKER_RECEIVE_WAITFOR)' - WHEN 14 THEN N'LOGMGR, LOGBUFFER, LOGMGR_RESERVE_APPEND, LOGMGR_FLUSH, LOGMGR_PMM_LOG, CHKPT, WRITELOG' - WHEN 15 THEN N'ASYNC_NETWORK_IO, NET_WAITFOR_PACKET, PROXY_NETWORK_IO, EXTERNAL_SCRIPT_NETWORK_IOF' - WHEN 16 THEN N'CXPACKET, EXCHANGE' - WHEN 17 THEN N'RESOURCE_SEMAPHORE, CMEMTHREAD, CMEMPARTITIONED, EE_PMOLOCK, MEMORY_ALLOCATION_EXT, RESERVED_MEMORY_ALLOCATION_EXT, MEMORY_GRANT_UPDATE' - WHEN 18 THEN N'WAITFOR, WAIT_FOR_RESULTS, BROKER_RECEIVE_WAITFOR' - WHEN 19 THEN N'TRACEWRITE, SQLTRACE_LOCK, SQLTRACE_FILE_BUFFER, SQLTRACE_FILE_WRITE_IO_COMPLETION, SQLTRACE_FILE_READ_IO_COMPLETION, SQLTRACE_PENDING_BUFFER_WRITERS, SQLTRACE_SHUTDOWN, QUERY_TRACEOUT, TRACE_EVTNOTIFF' - WHEN 20 THEN N'FT_RESTART_CRAWL, FULLTEXT GATHERER, MSSEARCH, FT_METADATA_MUTEX, FT_IFTSHC_MUTEX, FT_IFTSISM_MUTEX, FT_IFTS_RWLOCK, FT_COMPROWSET_RWLOCK, FT_MASTER_MERGE, FT_PROPERTYLIST_CACHE, FT_MASTER_MERGE_COORDINATOR, PWAIT_RESOURCE_SEMAPHORE_FT_PARALLEL_QUERY_SYNC' - WHEN 21 THEN N'ASYNC_IO_COMPLETION, IO_COMPLETION, BACKUPIO, WRITE_COMPLETION, IO_QUEUE_LIMIT, IO_RETRY' - WHEN 22 THEN N'SE_REPL_%, REPL_%, HADR_% (but not HADR_THROTTLE_LOG_RATE_GOVERNOR), PWAIT_HADR_%, REPLICA_WRITES, FCB_REPLICA_WRITE, FCB_REPLICA_READ, PWAIT_HADRSIM' - WHEN 23 THEN N'LOG_RATE_GOVERNOR, POOL_LOG_RATE_GOVERNOR, HADR_THROTTLE_LOG_RATE_GOVERNOR, INSTANCE_LOG_RATE_GOVERNOR' - END, - INDEX wws_ix_ids CLUSTERED ( plan_id) -); - - -/* -The next three tables hold plan XML parsed out to different degrees -*/ -DROP TABLE IF EXISTS #statements; - -CREATE TABLE #statements -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - statement XML, - INDEX s_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); - - -DROP TABLE IF EXISTS #query_plan; - -CREATE TABLE #query_plan -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - query_plan XML, - INDEX qp_ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); - - -DROP TABLE IF EXISTS #relop; - -CREATE TABLE #relop -( - plan_id BIGINT, - query_id BIGINT, - query_hash BINARY(8), - sql_handle VARBINARY(64), - relop XML, - INDEX ix_ids CLUSTERED (plan_id, query_id, query_hash, sql_handle) -); - - -DROP TABLE IF EXISTS #plan_cost; - -CREATE TABLE #plan_cost -( - query_plan_cost DECIMAL(38,2), - sql_handle VARBINARY(64), - plan_id INT, - INDEX px_ix_ids CLUSTERED (sql_handle, plan_id) -); - - -DROP TABLE IF EXISTS #est_rows; - -CREATE TABLE #est_rows -( - estimated_rows DECIMAL(38,2), - query_hash BINARY(8), - INDEX px_ix_ids CLUSTERED (query_hash) -); - - -DROP TABLE IF EXISTS #stats_agg; - -CREATE TABLE #stats_agg -( - sql_handle VARBINARY(64), - last_update DATETIME2, - modification_count DECIMAL(38, 2), - sampling_percent DECIMAL(38, 2), - [statistics] NVARCHAR(256), - [table] NVARCHAR(256), - [schema] NVARCHAR(256), - [database] NVARCHAR(256), - INDEX sa_ix_ids CLUSTERED (sql_handle) -); - - -DROP TABLE IF EXISTS #trace_flags; - -CREATE TABLE #trace_flags -( - sql_handle VARBINARY(54), - global_trace_flags NVARCHAR(4000), - session_trace_flags NVARCHAR(4000), - INDEX tf_ix_ids CLUSTERED (sql_handle) -); - - -DROP TABLE IF EXISTS #warning_results; - -CREATE TABLE #warning_results -( - ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED, - CheckID INT, - Priority TINYINT, - FindingsGroup NVARCHAR(50), - Finding NVARCHAR(200), - URL NVARCHAR(200), - Details NVARCHAR(4000) -); - -/*These next three tables hold information about implicit conversion and cached parameters */ -DROP TABLE IF EXISTS #stored_proc_info; - -CREATE TABLE #stored_proc_info -( - sql_handle VARBINARY(64), - query_hash BINARY(8), - variable_name NVARCHAR(128), - variable_datatype NVARCHAR(128), - converted_column_name NVARCHAR(128), - compile_time_value NVARCHAR(128), - proc_name NVARCHAR(300), - column_name NVARCHAR(128), - converted_to NVARCHAR(128) - INDEX tf_ix_ids CLUSTERED (sql_handle, query_hash) -); - -DROP TABLE IF EXISTS #variable_info - -CREATE TABLE #variable_info -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_name NVARCHAR(128), - variable_name NVARCHAR(200), - variable_datatype NVARCHAR(128), - compile_time_value NVARCHAR(4000), - INDEX vif_ix_ids CLUSTERED (sql_handle, query_hash) -); - -DROP TABLE IF EXISTS #conversion_info - -CREATE TABLE #conversion_info -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - proc_name NVARCHAR(128), - expression NVARCHAR(4000), - at_charindex AS CHARINDEX('@', expression), - bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), - comma_charindex AS CHARINDEX(',', expression) + 1, - second_comma_charindex AS - CHARINDEX(',', expression, CHARINDEX(',', expression) + 1) - CHARINDEX(',', expression) - 1, - equal_charindex AS CHARINDEX('=', expression) + 1, - paren_charindex AS CHARINDEX('(', expression) + 1, - comma_paren_charindex AS - CHARINDEX(',', expression, CHARINDEX('(', expression) + 1) - CHARINDEX('(', expression) - 1, - convert_implicit_charindex AS CHARINDEX('=CONVERT_IMPLICIT', expression), - INDEX cif_ix_ids CLUSTERED (sql_handle, query_hash) -); - -/* These tables support the Missing Index details clickable*/ - - -DROP TABLE IF EXISTS #missing_index_xml - -CREATE TABLE #missing_index_xml -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - index_xml XML -); - -DROP TABLE IF EXISTS #missing_index_schema - -CREATE TABLE #missing_index_schema -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - index_xml XML -); - - -DROP TABLE IF EXISTS #missing_index_usage - -CREATE TABLE #missing_index_usage -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - index_xml XML -); - -DROP TABLE IF EXISTS #missing_index_detail - -CREATE TABLE #missing_index_detail -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - usage NVARCHAR(128), - column_name NVARCHAR(128) -); - - -DROP TABLE IF EXISTS #missing_index_pretty - -CREATE TABLE #missing_index_pretty -( - query_hash BINARY(8), - sql_handle VARBINARY(64), - impact FLOAT, - database_name NVARCHAR(128), - schema_name NVARCHAR(128), - table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), - details AS N'/* ' - + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' - + CONVERT(NVARCHAR(30), impact) - + '%.' - + CHAR(10) - + N'*/' - + CHAR(10) + CHAR(13) - + N'/* ' - + CHAR(10) - + N'USE ' - + database_name - + CHAR(10) - + N'GO' - + CHAR(10) + CHAR(13) - + N'CREATE NONCLUSTERED INDEX ix_' - + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') - + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END - + CHAR(10) - + N' ON ' - + schema_name - + N'.' - + table_name - + N' (' + - + CASE WHEN equality IS NOT NULL - THEN equality - + CASE WHEN inequality IS NOT NULL - THEN N', ' + inequality - ELSE N'' - END - ELSE inequality - END - + N')' - + CHAR(10) - + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' - END - + CHAR(10) - + N'GO' - + CHAR(10) - + N'*/' -); - -/*Sets up WHERE clause that gets used quite a bit*/ - ---Date stuff ---If they're both NULL, we'll just look at the last 7 days -IF (@StartDate IS NULL AND @EndDate IS NULL) - BEGIN - RAISERROR(N'@StartDate and @EndDate are NULL, checking last 7 days', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= DATEADD(DAY, -7, DATEDIFF(DAY, 0, SYSDATETIME() )) - '; - END; - ---Hey, that's nice of me -IF @StartDate IS NOT NULL - BEGIN - RAISERROR(N'Setting start date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= @sp_StartDate - '; - END; - ---Alright, sensible -IF @EndDate IS NOT NULL - BEGIN - RAISERROR(N'Setting end date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time < @sp_EndDate - '; - END; - ---C'mon, why would you do that? -IF (@StartDate IS NULL AND @EndDate IS NOT NULL) - BEGIN - RAISERROR(N'Setting reasonable start date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time >= DATEADD(DAY, -7, @sp_EndDate) - '; - END; - ---Jeez, abusive -IF (@StartDate IS NOT NULL AND @EndDate IS NULL) - BEGIN - RAISERROR(N'Setting reasonable end date filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.last_execution_time < DATEADD(DAY, 7, @sp_StartDate) - '; - END; - ---I care about minimum execution counts -IF @MinimumExecutionCount IS NOT NULL - BEGIN - RAISERROR(N'Setting execution filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.count_executions >= @sp_MinimumExecutionCount - '; - END; - ---You care about stored proc names -IF @StoredProcName IS NOT NULL - BEGIN - RAISERROR(N'Setting stored proc filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N')) = @sp_StoredProcName - '; - END; - ---I will always love you, but hopefully this query will eventually end -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND (qsrs.avg_duration / 1000.) >= @sp_MinDuration - '; - END; - ---I don't know why you'd go looking for failed queries, but hey -IF (@Failed = 0 OR @Failed IS NULL) - BEGIN - RAISERROR(N'Setting failed query filter to 0', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.execution_type = 0 - '; - END; -IF (@Failed = 1) - BEGIN - RAISERROR(N'Setting failed query filter to 3, 4', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsrs.execution_type IN (3, 4) - '; - END; - -/*Filtering for plan_id or query_id*/ -IF (@PlanIdFilter IS NOT NULL) - BEGIN - RAISERROR(N'Setting plan_id filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsp.plan_id = @sp_PlanIdFilter - '; - END; - -IF (@QueryIdFilter IS NOT NULL) - BEGIN - RAISERROR(N'Setting query_id filter', 0, 1) WITH NOWAIT; - SET @sql_where += N' AND qsq.query_id = @sp_QueryIdFilter - '; - END; - - - -IF @Debug = 1 - RAISERROR(N'Starting WHERE clause:', 0, 1) WITH NOWAIT; - PRINT @sql_where; - -IF @sql_where IS NULL - BEGIN - RAISERROR(N'@sql_where is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -IF (@ExportToExcel = 1 OR @SkipXML = 1) - BEGIN - RAISERROR(N'Exporting to Excel or skipping XML, hiding summary', 0, 1) WITH NOWAIT; - SET @HideSummary = 1; - END; - - -IF @StoredProcName IS NOT NULL - BEGIN - - DECLARE @sql NVARCHAR(MAX); - DECLARE @out INT; - DECLARE @proc_params NVARCHAR(MAX) = N'@sp_StartDate DATETIME2, @sp_EndDate DATETIME2, @sp_MinimumExecutionCount INT, @sp_MinDuration INT, @sp_StoredProcName NVARCHAR(128), @sp_PlanIdFilter INT, @sp_QueryIdFilter INT, @i_out INT OUTPUT'; - - - SET @sql = N'SELECT @i_out = COUNT(*) - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp - ON qsp.plan_id = qsrs.plan_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq - ON qsq.query_id = qsp.query_id - WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - - SET @sql += @sql_where; - - EXEC sys.sp_executesql @sql, - @proc_params, - @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter, @i_out = @out OUTPUT; - - IF @out = 0 - BEGIN - - SET @msg = N'We couldn''t find the Stored Procedure ' + QUOTENAME(@StoredProcName) + N' in the Query Store views for ' + QUOTENAME(@DatabaseName) + N' between ' + CONVERT(NVARCHAR(30), ISNULL(@StartDate, DATEADD(DAY, -7, DATEDIFF(DAY, 0, SYSDATETIME() ))) ) + N' and ' + CONVERT(NVARCHAR(30), ISNULL(@EndDate, SYSDATETIME())) + - '. Try removing schema prefixes or adjusting dates. If it was executed from a different database context, try searching there instead.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - - SELECT @msg AS [Blue Flowers, Blue Flowers, Blue Flowers]; - - RETURN; - - END; - - END; - - - - -/* -This is our grouped interval query. - -By default, it looks at queries: - In the last 7 days - That aren't system queries - That have a query plan (some won't, if nested level is > 128, along with other reasons) - And haven't failed - This stuff, along with some other options, will be configurable in the stored proc - -*/ - -IF @sql_where IS NOT NULL -BEGIN TRY - BEGIN - - RAISERROR(N'Populating temp tables', 0, 1) WITH NOWAIT; - -RAISERROR(N'Gathering intervals', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT CONVERT(DATE, qsrs.last_execution_time) AS flat_date, - MIN(DATEADD(HOUR, DATEDIFF(HOUR, 0, qsrs.last_execution_time), 0)) AS start_range, - MAX(DATEADD(HOUR, DATEDIFF(HOUR, 0, qsrs.last_execution_time) + 1, 0)) AS end_range, - SUM(qsrs.avg_duration / 1000.) / SUM(qsrs.count_executions) AS total_avg_duration_ms, - SUM(qsrs.avg_cpu_time / 1000.) / SUM(qsrs.count_executions) AS total_avg_cpu_time_ms, - SUM((qsrs.avg_logical_io_reads * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_reads_mb, - SUM((qsrs.avg_physical_io_reads* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_physical_io_reads_mb, - SUM((qsrs.avg_logical_io_writes* 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_logical_io_writes_mb, - SUM(( qsrs.avg_query_max_used_memory * 8 ) / 1024.) / SUM(qsrs.count_executions) AS total_avg_query_max_used_memory_mb, - SUM(qsrs.avg_rowcount) AS total_rowcount, - SUM(qsrs.count_executions) AS total_count_executions' - IF @new_columns = 1 - BEGIN - SET @sql_select += N', - SUM((qsrs.avg_log_bytes_used) / 1048576.) / SUM(qsrs.count_executions) AS total_avg_log_bytes_mb, - SUM(avg_tempdb_space_used) / SUM(qsrs.count_executions) AS total_avg_tempdb_space - ' - END - IF @new_columns = 0 - BEGIN - SET @sql_select += N', - NULL AS total_avg_log_bytes_mb, - NULL AS total_avg_tempdb_space - ' - END - - -SET @sql_select += N'FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp - ON qsp.plan_id = qsrs.plan_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq - ON qsq.query_id = qsp.query_id - WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - - -SET @sql_select += @sql_where; - -SET @sql_select += - N'GROUP BY CONVERT(DATE, qsrs.last_execution_time) - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #grouped_interval WITH (TABLOCK) - ( flat_date, start_range, end_range, total_avg_duration_ms, - total_avg_cpu_time_ms, total_avg_logical_io_reads_mb, total_avg_physical_io_reads_mb, - total_avg_logical_io_writes_mb, total_avg_query_max_used_memory_mb, total_rowcount, - total_count_executions, total_avg_log_bytes_mb, total_avg_tempdb_space ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - -/* -The next group of queries looks at plans in the ranges we found in the grouped interval query - -We take the highest value from each metric (duration, cpu, etc) and find the top plans by that metric in the range - -They insert into the #working_plans table -*/ - - - -/*Get longest duration plans*/ - -RAISERROR(N'Gathering longest duration plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH duration_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_duration_ms DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''duration'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN duration_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_duration DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get longest cpu plans*/ - -RAISERROR(N'Gathering highest cpu plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH cpu_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_cpu_time_ms DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''cpu'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN cpu_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_cpu_time DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest logical read plans*/ - -RAISERROR(N'Gathering highest logical read plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH logical_reads_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_logical_io_reads_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''logical reads'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN logical_reads_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_logical_io_reads DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest physical read plans*/ - -RAISERROR(N'Gathering highest physical read plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH physical_read_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_physical_io_reads_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''physical reads'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN physical_read_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_physical_io_reads DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest logical write plans*/ - -RAISERROR(N'Gathering highest write plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH logical_writes_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_logical_io_writes_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''writes'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN logical_writes_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_logical_io_writes DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest memory use plans*/ - -RAISERROR(N'Gathering highest memory use plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH memory_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_query_max_used_memory_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''memory'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN memory_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_query_max_used_memory DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*Get highest row count plans*/ - -RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_rowcount DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''rows'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_rowcount DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - -IF @new_columns = 1 -BEGIN - -RAISERROR(N'Gathering new 2017 new column info...', 0, 1) WITH NOWAIT; - -/*Get highest log byte count plans*/ - -RAISERROR(N'Gathering highest log byte use plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_log_bytes_mb DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''log bytes'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_log_bytes_used DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -/*Get highest row count plans*/ - -RAISERROR(N'Gathering highest row count plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -WITH rowcount_max -AS ( SELECT TOP 1 - gi.start_range, - gi.end_range - FROM #grouped_interval AS gi - ORDER BY gi.total_avg_tempdb_space DESC ) -INSERT #working_plans WITH (TABLOCK) - ( plan_id, query_id, pattern ) -SELECT TOP ( @sp_Top ) - qsp.plan_id, qsp.query_id, ''tempdb space'' -FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -JOIN rowcount_max AS dm -ON qsp.last_execution_time >= dm.start_range - AND qsp.last_execution_time < dm.end_range -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = qsp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON qsq.query_id = qsp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'ORDER BY qsrs.avg_tempdb_space_used DESC - OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -END; - - -/* -This rolls up the different patterns we find before deduplicating. - -The point of this is so we know if a query was gathered by one or more of the search queries - -*/ - -RAISERROR(N'Updating patterns', 0, 1) WITH NOWAIT; - -WITH patterns AS ( -SELECT wp.plan_id, wp.query_id, - pattern_path = STUFF((SELECT DISTINCT N', ' + wp2.pattern - FROM #working_plans AS wp2 - WHERE wp.plan_id = wp2.plan_id - AND wp.query_id = wp2.query_id - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') -FROM #working_plans AS wp -) -UPDATE wp -SET wp.pattern = patterns.pattern_path -FROM #working_plans AS wp -JOIN patterns -ON wp.plan_id = patterns.plan_id -AND wp.query_id = patterns.query_id -OPTION (RECOMPILE); - - -/* -This dedupes our results so we hopefully don't double-work the same plan -*/ - -RAISERROR(N'Deduplicating gathered plans', 0, 1) WITH NOWAIT; - -WITH dedupe AS ( -SELECT * , ROW_NUMBER() OVER (PARTITION BY wp.plan_id ORDER BY wp.plan_id) AS dupes -FROM #working_plans AS wp -) -DELETE dedupe -WHERE dedupe.dupes > 1 -OPTION (RECOMPILE); - -SET @msg = N'Removed ' + CONVERT(NVARCHAR(10), @@ROWCOUNT) + N' duplicate plan_ids.'; -RAISERROR(@msg, 0, 1) WITH NOWAIT; - - -/* -This gathers data for the #working_metrics table -*/ - - -RAISERROR(N'Collecting worker metrics', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT ' + QUOTENAME(@DatabaseName, '''') + N' AS database_name, wp.plan_id, wp.query_id, - QUOTENAME(object_schema_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) + ''.'' + - QUOTENAME(object_name(qsq.object_id, DB_ID(' + QUOTENAME(@DatabaseName, '''') + N'))) AS proc_or_function_name, - qsq.batch_sql_handle, qsq.query_hash, qsq.query_parameterization_type_desc, qsq.count_compiles, - (qsq.avg_compile_duration / 1000.), - (qsq.last_compile_duration / 1000.), - (qsq.avg_bind_duration / 1000.), - (qsq.last_bind_duration / 1000.), - (qsq.avg_bind_cpu_time / 1000.), - (qsq.last_bind_cpu_time / 1000.), - (qsq.avg_optimize_duration / 1000.), - (qsq.last_optimize_duration / 1000.), - (qsq.avg_optimize_cpu_time / 1000.), - (qsq.last_optimize_cpu_time / 1000.), - (qsq.avg_compile_memory_kb / 1024.), - (qsq.last_compile_memory_kb / 1024.), - qsrs.execution_type_desc, qsrs.first_execution_time, qsrs.last_execution_time, qsrs.count_executions, - (qsrs.avg_duration / 1000.), - (qsrs.last_duration / 1000.), - (qsrs.min_duration / 1000.), - (qsrs.max_duration / 1000.), - (qsrs.avg_cpu_time / 1000.), - (qsrs.last_cpu_time / 1000.), - (qsrs.min_cpu_time / 1000.), - (qsrs.max_cpu_time / 1000.), - ((qsrs.avg_logical_io_reads * 8 ) / 1024.), - ((qsrs.last_logical_io_reads * 8 ) / 1024.), - ((qsrs.min_logical_io_reads * 8 ) / 1024.), - ((qsrs.max_logical_io_reads * 8 ) / 1024.), - ((qsrs.avg_logical_io_writes * 8 ) / 1024.), - ((qsrs.last_logical_io_writes * 8 ) / 1024.), - ((qsrs.min_logical_io_writes * 8 ) / 1024.), - ((qsrs.max_logical_io_writes * 8 ) / 1024.), - ((qsrs.avg_physical_io_reads * 8 ) / 1024.), - ((qsrs.last_physical_io_reads * 8 ) / 1024.), - ((qsrs.min_physical_io_reads * 8 ) / 1024.), - ((qsrs.max_physical_io_reads * 8 ) / 1024.), - (qsrs.avg_clr_time / 1000.), - (qsrs.last_clr_time / 1000.), - (qsrs.min_clr_time / 1000.), - (qsrs.max_clr_time / 1000.), - qsrs.avg_dop, qsrs.last_dop, qsrs.min_dop, qsrs.max_dop, - ((qsrs.avg_query_max_used_memory * 8 ) / 1024.), - ((qsrs.last_query_max_used_memory * 8 ) / 1024.), - ((qsrs.min_query_max_used_memory * 8 ) / 1024.), - ((qsrs.max_query_max_used_memory * 8 ) / 1024.), - qsrs.avg_rowcount, qsrs.last_rowcount, qsrs.min_rowcount, qsrs.max_rowcount,'; - - IF @new_columns = 1 - BEGIN - SET @sql_select += N' - qsrs.avg_num_physical_io_reads, qsrs.last_num_physical_io_reads, qsrs.min_num_physical_io_reads, qsrs.max_num_physical_io_reads, - (qsrs.avg_log_bytes_used / 100000000), - (qsrs.last_log_bytes_used / 100000000), - (qsrs.min_log_bytes_used / 100000000), - (qsrs.max_log_bytes_used / 100000000), - ((qsrs.avg_tempdb_space_used * 8 ) / 1024.), - ((qsrs.last_tempdb_space_used * 8 ) / 1024.), - ((qsrs.min_tempdb_space_used * 8 ) / 1024.), - ((qsrs.max_tempdb_space_used * 8 ) / 1024.) - '; - END; - IF @new_columns = 0 - BEGIN - SET @sql_select += N' - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL, - NULL - '; - END; -SET @sql_select += -N'FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id -AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #working_metrics WITH (TABLOCK) - ( database_name, plan_id, query_id, - proc_or_function_name, - batch_sql_handle, query_hash, query_parameterization_type_desc, count_compiles, - avg_compile_duration, last_compile_duration, avg_bind_duration, last_bind_duration, avg_bind_cpu_time, last_bind_cpu_time, avg_optimize_duration, - last_optimize_duration, avg_optimize_cpu_time, last_optimize_cpu_time, avg_compile_memory_kb, last_compile_memory_kb, execution_type_desc, - first_execution_time, last_execution_time, count_executions, avg_duration, last_duration, min_duration, max_duration, avg_cpu_time, last_cpu_time, - min_cpu_time, max_cpu_time, avg_logical_io_reads, last_logical_io_reads, min_logical_io_reads, max_logical_io_reads, avg_logical_io_writes, - last_logical_io_writes, min_logical_io_writes, max_logical_io_writes, avg_physical_io_reads, last_physical_io_reads, min_physical_io_reads, - max_physical_io_reads, avg_clr_time, last_clr_time, min_clr_time, max_clr_time, avg_dop, last_dop, min_dop, max_dop, avg_query_max_used_memory, - last_query_max_used_memory, min_query_max_used_memory, max_query_max_used_memory, avg_rowcount, last_rowcount, min_rowcount, max_rowcount, - /* 2017 only columns */ - avg_num_physical_io_reads, last_num_physical_io_reads, min_num_physical_io_reads, max_num_physical_io_reads, - avg_log_bytes_used, last_log_bytes_used, min_log_bytes_used, max_log_bytes_used, - avg_tempdb_space_used, last_tempdb_space_used, min_tempdb_space_used, max_tempdb_space_used ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/*This just helps us classify our queries*/ -UPDATE #working_metrics -SET proc_or_function_name = N'Statement' -WHERE proc_or_function_name IS NULL; - - -/* -This gathers data for the #working_plan_text table -*/ - - -RAISERROR(N'Gathering working plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT ' + QUOTENAME(@DatabaseName, '''') + N' AS database_name, wp.plan_id, wp.query_id, - qsp.plan_group_id, qsp.engine_version, qsp.compatibility_level, qsp.query_plan_hash, TRY_CONVERT(XML, qsp.query_plan), qsp.is_online_index_plan, qsp.is_trivial_plan, - qsp.is_parallel_plan, qsp.is_forced_plan, qsp.is_natively_compiled, qsp.force_failure_count, qsp.last_force_failure_reason_desc, qsp.count_compiles, - qsp.initial_compile_start_time, qsp.last_compile_start_time, qsp.last_execution_time, - (qsp.avg_compile_duration / 1000.), - (qsp.last_compile_duration / 1000.), - qsqt.query_sql_text, qsqt.statement_sql_handle, qsqt.is_part_of_encrypted_module, qsqt.has_restricted_text -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #working_plan_text WITH (TABLOCK) - ( database_name, plan_id, query_id, - plan_group_id, engine_version, compatibility_level, query_plan_hash, query_plan_xml, is_online_index_plan, is_trivial_plan, - is_parallel_plan, is_forced_plan, is_natively_compiled, force_failure_count, last_force_failure_reason_desc, count_compiles, - initial_compile_start_time, last_compile_start_time, last_execution_time, avg_compile_duration, last_compile_duration, - query_sql_text, statement_sql_handle, is_part_of_encrypted_module, has_restricted_text ) - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - - - -/* - -Some memory grant information isn't available in query store - -We have to go back to other DMVs to find it, when possible - -It may not be there for various reaons - -*/ -RAISERROR(N'Checking dm_exec_query_stats for memory grant info', 0, 1) WITH NOWAIT; -WITH max_mem -AS ( SELECT deqs.sql_handle, MAX(deqs.min_grant_kb) AS min_grant_kb, MAX(deqs.max_used_grant_kb) AS max_used_grant_kb - FROM sys.dm_exec_query_stats AS deqs - GROUP BY deqs.sql_handle ) -UPDATE wpt -SET wpt.min_grant_kb = deqs.min_grant_kb, - wpt.max_used_grant_kb = deqs.max_used_grant_kb -FROM #working_plan_text AS wpt -JOIN max_mem AS deqs -ON wpt.statement_sql_handle = deqs.sql_handle -OPTION (RECOMPILE); - - -/* -This gets us context settings for our queries and adds it to the #working_plan_text table -*/ - -RAISERROR(N'Gathering context settings', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -UPDATE wp -SET wp.context_settings = SUBSTRING( - CASE WHEN (CAST(qcs.set_options AS INT) & 1 = 1) THEN '', ANSI_PADDING'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 8 = 8) THEN '', CONCAT_NULL_YIELDS_NULL'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 16 = 16) THEN '', ANSI_WARNINGS'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 32 = 32) THEN '', ANSI_NULLS'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 64 = 64) THEN '', QUOTED_IDENTIFIER'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 4096 = 4096) THEN '', ARITH_ABORT'' ELSE '''' END + - CASE WHEN (CAST(qcs.set_options AS INT) & 8192 = 8192) THEN '', NUMERIC_ROUNDABORT'' ELSE '''' END - , 2, 200000) -FROM #working_plan_text wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_context_settings AS qcs -ON qcs.context_settings_id = qsq.context_settings_id -OPTION (RECOMPILE); -'; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select; - - -/*This adds the patterns we found from each interval to the #working_plan_text table*/ - -RAISERROR(N'Add patterns to working plans', 0, 1) WITH NOWAIT; - -UPDATE wpt -SET wpt.pattern = wp.pattern -FROM #working_plans AS wp -JOIN #working_plan_text AS wpt -ON wpt.plan_id = wp.plan_id -AND wpt.query_id = wp.query_id -OPTION (RECOMPILE); - -/*This cleans up query text a bit*/ - -RAISERROR(N'Clean awkward characters from query text', 0, 1) WITH NOWAIT; - -UPDATE b -SET b.query_sql_text = REPLACE(REPLACE(REPLACE(b.query_sql_text, @cr, ' '), @lf, ' '), @tab, ' ') -FROM #working_plan_text AS b -OPTION (RECOMPILE); - - -/*This populates #working_wait_stats when available*/ - -IF @waitstats = 1 - - BEGIN - - RAISERROR(N'Collecting wait stats info', 0, 1) WITH NOWAIT; - - - SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; - SET @sql_select += N' - SELECT qws.plan_id, - qws.wait_category, - qws.wait_category_desc, - SUM(qws.total_query_wait_time_ms) AS total_query_wait_time_ms, - SUM(qws.avg_query_wait_time_ms) AS avg_query_wait_time_ms, - SUM(qws.last_query_wait_time_ms) AS last_query_wait_time_ms, - SUM(qws.min_query_wait_time_ms) AS min_query_wait_time_ms, - SUM(qws.max_query_wait_time_ms) AS max_query_wait_time_ms - FROM ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_wait_stats qws - JOIN #working_plans AS wp - ON qws.plan_id = wp.plan_id - GROUP BY qws.plan_id, qws.wait_category, qws.wait_category_desc - HAVING SUM(qws.min_query_wait_time_ms) >= 5 - OPTION (RECOMPILE); - '; - - IF @Debug = 1 - PRINT @sql_select; - - IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - - INSERT #working_wait_stats WITH (TABLOCK) - ( plan_id, wait_category, wait_category_desc, total_query_wait_time_ms, avg_query_wait_time_ms, last_query_wait_time_ms, min_query_wait_time_ms, max_query_wait_time_ms ) - - EXEC sys.sp_executesql @stmt = @sql_select; - - - /*This updates #working_plan_text with the top three waits from the wait stats DMV*/ - - RAISERROR(N'Update working_plan_text with top three waits', 0, 1) WITH NOWAIT; - - - UPDATE wpt - SET wpt.top_three_waits = x.top_three_waits - FROM #working_plan_text AS wpt - JOIN ( - SELECT wws.plan_id, - top_three_waits = STUFF((SELECT TOP 3 N', ' + wws2.wait_category_desc + N' (' + CONVERT(NVARCHAR(20), SUM(CONVERT(BIGINT, wws2.avg_query_wait_time_ms))) + N' ms) ' - FROM #working_wait_stats AS wws2 - WHERE wws.plan_id = wws2.plan_id - GROUP BY wws2.wait_category_desc - ORDER BY SUM(wws2.avg_query_wait_time_ms) DESC - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') - FROM #working_wait_stats AS wws - GROUP BY wws.plan_id - ) AS x - ON x.plan_id = wpt.plan_id - OPTION (RECOMPILE); - -END; - -/*End wait stats population*/ - -UPDATE #working_plan_text -SET top_three_waits = CASE - WHEN @waitstats = 0 THEN N'The query store waits stats DMV is not available' - ELSE N'No Significant waits detected!' - END -WHERE top_three_waits IS NULL; - -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure populating temp tables.', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -IF (@SkipXML = 0) -BEGIN TRY -BEGIN - -/* -This sets up the #working_warnings table with the IDs we're interested in so we can tie warnings back to them -*/ - -RAISERROR(N'Populate working warnings table with gathered plans', 0, 1) WITH NOWAIT; - - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -SELECT wp.plan_id, wp.query_id, qsq.query_hash, qsqt.statement_sql_handle -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'OPTION (RECOMPILE); - '; - -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -INSERT #working_warnings WITH (TABLOCK) - ( plan_id, query_id, query_hash, sql_handle ) -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -/* -This looks for queries in the query stores that we picked up from an internal that have multiple plans in cache - -This and several of the following queries all replaced XML parsing to find plan attributes. Sweet. - -Thanks, Query Store -*/ - -RAISERROR(N'Populating object name in #working_warnings', 0, 1) WITH NOWAIT; -UPDATE w -SET w.proc_or_function_name = ISNULL(wm.proc_or_function_name, N'Statement') -FROM #working_warnings AS w -JOIN #working_metrics AS wm -ON w.plan_id = wm.plan_id - AND w.query_id = wm.query_id -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for multiple plans', 0, 1) WITH NOWAIT; - -SET @sql_select = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;'; -SET @sql_select += N' -UPDATE ww -SET ww.plan_multiple_plans = 1 -FROM #working_warnings AS ww -JOIN -( -SELECT wp.query_id, COUNT(qsp.plan_id) AS plans -FROM #working_plans AS wp -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_plan AS qsp -ON qsp.plan_id = wp.plan_id - AND qsp.query_id = wp.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query AS qsq -ON wp.query_id = qsq.query_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_query_text AS qsqt -ON qsqt.query_text_id = qsq.query_text_id -JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.query_store_runtime_stats AS qsrs -ON qsrs.plan_id = wp.plan_id -WHERE 1 = 1 - AND qsq.is_internal_query = 0 - AND qsp.query_plan IS NOT NULL - '; - -SET @sql_select += @sql_where; - -SET @sql_select += N'GROUP BY wp.query_id - HAVING COUNT(qsp.plan_id) > 1 - ) AS x - ON ww.query_id = x.query_id - OPTION (RECOMPILE); - '; -IF @Debug = 1 - PRINT @sql_select; - -IF @sql_select IS NULL - BEGIN - RAISERROR(N'@sql_select is NULL', 0, 1) WITH NOWAIT; - RETURN; - END; - -EXEC sys.sp_executesql @stmt = @sql_select, - @params = @sp_params, - @sp_Top = @Top, @sp_StartDate = @StartDate, @sp_EndDate = @EndDate, @sp_MinimumExecutionCount = @MinimumExecutionCount, @sp_MinDuration = @duration_filter_ms, @sp_StoredProcName = @StoredProcName, @sp_PlanIdFilter = @PlanIdFilter, @sp_QueryIdFilter = @QueryIdFilter; - -/* -This looks for forced plans -*/ - -RAISERROR(N'Checking for forced plans', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_forced_plan = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.is_forced_plan = 1 -OPTION (RECOMPILE); - - -/* -This looks for forced parameterization -*/ - -RAISERROR(N'Checking for forced parameterization', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_forced_parameterized = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.query_parameterization_type_desc = 'Forced' -OPTION (RECOMPILE); - - -/* -This looks for unparameterized queries -*/ - -RAISERROR(N'Checking for unparameterized plans', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.unparameterized_query = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.query_parameterization_type_desc = 'None' - AND ww.proc_or_function_name = 'Statement' -OPTION (RECOMPILE); - - -/* -This looks for cursors -*/ - -RAISERROR(N'Checking for cursors', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_cursor = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.plan_group_id > 0 -OPTION (RECOMPILE); - - -/* -This looks for parallel plans -*/ -UPDATE ww -SET ww.is_parallel = 1 -FROM #working_warnings AS ww -JOIN #working_plan_text AS wp -ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - AND wp.is_parallel_plan = 1 -OPTION (RECOMPILE); - -/*This looks for old CE*/ - -RAISERROR(N'Checking for legacy CE', 0, 1) WITH NOWAIT; - -UPDATE w -SET w.downlevel_estimator = 1 -FROM #working_warnings AS w -JOIN #working_plan_text AS wpt -ON w.plan_id = wpt.plan_id -AND w.query_id = wpt.query_id -/*PLEASE DON'T TELL ANYONE I DID THIS*/ -WHERE PARSENAME(wpt.engine_version, 4) < PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4) -OPTION (RECOMPILE); -/*NO SERIOUSLY THIS IS A HORRIBLE IDEA*/ - - -/*This looks for trivial plans*/ - -RAISERROR(N'Checking for trivial plans', 0, 1) WITH NOWAIT; - -UPDATE w -SET w.is_trivial = 1 -FROM #working_warnings AS w -JOIN #working_plan_text AS wpt -ON w.plan_id = wpt.plan_id -AND w.query_id = wpt.query_id -AND wpt.is_trivial_plan = 1 -OPTION (RECOMPILE); - -/*Plans that compile 2x more than they execute*/ - -RAISERROR(N'Checking for plans that compile 2x more than they execute', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_compile_more = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND wm.count_compiles > (wm.count_executions * 2) -OPTION (RECOMPILE); - -/*Plans that compile 2x more than they execute*/ - -RAISERROR(N'Checking for plans that take more than 5 seconds to bind, compile, or optimize', 0, 1) WITH NOWAIT; - -UPDATE ww -SET ww.is_slow_plan = 1 -FROM #working_warnings AS ww -JOIN #working_metrics AS wm -ON ww.plan_id = wm.plan_id - AND ww.query_id = wm.query_id - AND (wm.avg_bind_duration > 5000 - OR - wm.avg_compile_duration > 5000 - OR - wm.avg_optimize_duration > 5000 - OR - wm.avg_optimize_cpu_time > 5000) -OPTION (RECOMPILE); - - - -/* -This parses the XML from our top plans into smaller chunks for easier consumption -*/ - -RAISERROR(N'Begin XML nodes parsing', 0, 1) WITH NOWAIT; - -RAISERROR(N'Inserting #statements', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement - FROM #working_warnings AS ww - JOIN #working_plan_text AS wp - ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - CROSS APPLY wp.query_plan_xml.nodes('//p:StmtSimple') AS q(n) -OPTION (RECOMPILE); - -RAISERROR(N'Inserting parsed cursor XML to #statements', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #statements WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, statement ) - SELECT ww.plan_id, ww.query_id, ww.query_hash, ww.sql_handle, q.n.query('.') AS statement - FROM #working_warnings AS ww - JOIN #working_plan_text AS wp - ON ww.plan_id = wp.plan_id - AND ww.query_id = wp.query_id - CROSS APPLY wp.query_plan_xml.nodes('//p:StmtCursor') AS q(n) -OPTION (RECOMPILE); - -RAISERROR(N'Inserting to #query_plan', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #query_plan WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, query_plan ) -SELECT s.plan_id, s.query_id, s.query_hash, s.sql_handle, q.n.query('.') AS query_plan -FROM #statements AS s - CROSS APPLY s.statement.nodes('//p:QueryPlan') AS q(n) -OPTION (RECOMPILE); - -RAISERROR(N'Inserting to #relop', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #relop WITH (TABLOCK) ( plan_id, query_id, query_hash, sql_handle, relop) -SELECT qp.plan_id, qp.query_id, qp.query_hash, qp.sql_handle, q.n.query('.') AS relop -FROM #query_plan qp - CROSS APPLY qp.query_plan.nodes('//p:RelOp') AS q(n) -OPTION (RECOMPILE); - - --- statement level checks - -RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.compile_timeout = 1 -FROM #statements s -JOIN #working_warnings AS b -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Performing compile memory limit exceeded checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.compile_memory_limit_exceeded = 1 -FROM #statements s -JOIN #working_warnings AS b -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -index_dml AS ( - SELECT s.query_hash, - index_dml = CASE WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="CREATE INDEX"]') = 1 THEN 1 - WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="DROP INDEX"]') = 1 THEN 1 - END - FROM #statements s - ) - UPDATE b - SET b.index_dml = i.index_dml - FROM #working_warnings AS b - JOIN index_dml i - ON i.query_hash = b.query_hash - WHERE i.index_dml = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -table_dml AS ( - SELECT s.query_hash, - table_dml = CASE WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="CREATE TABLE"]') = 1 THEN 1 - WHEN s.statement.exist('//p:StmtSimple/@StatementType[.="DROP OBJECT"]') = 1 THEN 1 - END - FROM #statements AS s - ) - UPDATE b - SET b.table_dml = t.table_dml - FROM #working_warnings AS b - JOIN table_dml t - ON t.query_hash = b.query_hash - WHERE t.table_dml = 1 -OPTION (RECOMPILE); - -RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -INSERT #est_rows (query_hash, estimated_rows) -SELECT DISTINCT - CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(c.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS query_hash, - c.n.value('(/p:StmtSimple/@StatementEstRows)[1]', 'FLOAT') AS estimated_rows -FROM #statements AS s -CROSS APPLY s.statement.nodes('/p:StmtSimple') AS c(n) -WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; - - UPDATE b - SET b.estimated_rows = er.estimated_rows - FROM #working_warnings AS b - JOIN #est_rows er - ON er.query_hash = b.query_hash - OPTION (RECOMPILE); - - -/*Begin plan cost calculations*/ -RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #plan_cost WITH (TABLOCK) - ( query_plan_cost, sql_handle, plan_id ) -SELECT DISTINCT - s.statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') query_plan_cost, - s.sql_handle, - s.plan_id -FROM #statements s -OUTER APPLY s.statement.nodes('/p:StmtSimple') AS q(n) -WHERE s.statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') > 0 -OPTION (RECOMPILE); - - -RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; -WITH pc AS ( - SELECT SUM(DISTINCT pc.query_plan_cost) AS queryplancostsum, pc.sql_handle, pc.plan_id - FROM #plan_cost AS pc - GROUP BY pc.sql_handle, pc.plan_id - ) - UPDATE b - SET b.query_cost = ISNULL(pc.queryplancostsum, 0) - FROM #working_warnings AS b - JOIN pc - ON pc.sql_handle = b.sql_handle - AND pc.plan_id = b.plan_id -OPTION (RECOMPILE); - - -/*End plan cost calculations*/ - - -RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.plan_warnings = 1 -FROM #query_plan qp -JOIN #working_warnings b -ON qp.sql_handle = b.sql_handle -AND qp.query_plan.exist('/p:QueryPlan/p:Warnings') = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.implicit_conversions = 1 -FROM #query_plan qp -JOIN #working_warnings b -ON qp.sql_handle = b.sql_handle -AND qp.query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - c.n.exist('//p:Warnings[(@NoJoinPredicate[.="1"])]') AS warning_no_join_predicate, - c.n.exist('//p:ColumnsWithNoStatistics') AS no_stats_warning , - c.n.exist('//p:Warnings') AS relop_warnings -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Warnings') AS c(n) -) -UPDATE b -SET b.warning_no_join_predicate = x.warning_no_join_predicate, - b.no_stats_warning = x.no_stats_warning, - b.relop_warnings = x.relop_warnings -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - c.n.value('substring(@Table, 2, 1)','VARCHAR(100)') AS first_char -FROM #relop r -CROSS APPLY r.relop.nodes('//p:Object') AS c(n) -) -UPDATE b -SET b.is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -JOIN #working_metrics AS wm -ON b.plan_id = wm.plan_id -AND b.query_id = wm.query_id -AND wm.batch_sql_handle IS NOT NULL -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, x AS ( -SELECT r.sql_handle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS function_count, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[@IsClrFunction = "1"]))', 'INT') AS clr_function_count -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -) -UPDATE b -SET b.function_count = x.function_count, - b.clr_function_count = x.clr_function_count -FROM #working_warnings b -JOIN x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.key_lookup_cost = x.key_lookup_cost -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost -FROM #relop r -WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.remote_query_cost = x.remote_query_cost -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost -FROM #relop r -WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.sort_cost = (x.sort_io + x.sort_cpu) -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - r.relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - r.relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop r -WHERE r.relop.exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM #working_warnings b -JOIN #statements AS s -ON b.sql_handle = s.sql_handle -AND b.is_cursor = 1 -CROSS APPLY s.statement.nodes('/p:StmtCursor') AS n(fn) -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET -b.is_table_scan = x.is_table_scan, -b.backwards_scan = x.backwards_scan, -b.forced_index = x.forced_index, -b.forced_seek = x.forced_seek, -b.forced_scan = x.forced_scan -FROM #working_warnings b -JOIN ( -SELECT - r.sql_handle, - 0 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop r -CROSS APPLY r.relop.nodes('//p:IndexScan') AS q(n) -UNION ALL -SELECT - r.sql_handle, - 1 AS is_table_scan, - q.n.exist('@ScanDirection[.="BACKWARD"]') AS backwards_scan, - q.n.value('@ForcedIndex', 'bit') AS forced_index, - q.n.value('@ForceSeek', 'bit') AS forced_seek, - q.n.value('@ForceScan', 'bit') AS forced_scan -FROM #relop r -CROSS APPLY r.relop.nodes('//p:TableScan') AS q(n) -) AS x ON b.sql_handle = x.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_computed_scalar = x.computed_column_function -FROM #working_warnings b -JOIN ( -SELECT r.sql_handle, - n.fn.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS computed_column_function -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) -WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_computed_filter = x.filter_function -FROM #working_warnings b -JOIN ( -SELECT -r.sql_handle, -c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))', 'INT') AS filter_function -FROM #relop AS r -CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) -) x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -IndexOps AS -( - SELECT - r.query_hash, - c.n.value('@PhysicalOp', 'VARCHAR(100)') AS op_name, - c.n.exist('@PhysicalOp[.="Index Insert"]') AS ii, - c.n.exist('@PhysicalOp[.="Index Update"]') AS iu, - c.n.exist('@PhysicalOp[.="Index Delete"]') AS id, - c.n.exist('@PhysicalOp[.="Clustered Index Insert"]') AS cii, - c.n.exist('@PhysicalOp[.="Clustered Index Update"]') AS ciu, - c.n.exist('@PhysicalOp[.="Clustered Index Delete"]') AS cid, - c.n.exist('@PhysicalOp[.="Table Insert"]') AS ti, - c.n.exist('@PhysicalOp[.="Table Update"]') AS tu, - c.n.exist('@PhysicalOp[.="Table Delete"]') AS td - FROM #relop AS r - CROSS APPLY r.relop.nodes('/p:RelOp') c(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:ScalarInsert/p:Object') q(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:Update/p:Object') o2(n) - OUTER APPLY r.relop.nodes('/p:RelOp/p:SimpleUpdate/p:Object') o3(n) -), iops AS -( - SELECT ios.query_hash, - SUM(CONVERT(TINYINT, ios.ii)) AS index_insert_count, - SUM(CONVERT(TINYINT, ios.iu)) AS index_update_count, - SUM(CONVERT(TINYINT, ios.id)) AS index_delete_count, - SUM(CONVERT(TINYINT, ios.cii)) AS cx_insert_count, - SUM(CONVERT(TINYINT, ios.ciu)) AS cx_update_count, - SUM(CONVERT(TINYINT, ios.cid)) AS cx_delete_count, - SUM(CONVERT(TINYINT, ios.ti)) AS table_insert_count, - SUM(CONVERT(TINYINT, ios.tu)) AS table_update_count, - SUM(CONVERT(TINYINT, ios.td)) AS table_delete_count - FROM IndexOps AS ios - WHERE ios.op_name IN ('Index Insert', 'Index Delete', 'Index Update', - 'Clustered Index Insert', 'Clustered Index Delete', 'Clustered Index Update', - 'Table Insert', 'Table Delete', 'Table Update') - GROUP BY ios.query_hash) -UPDATE b -SET b.index_insert_count = iops.index_insert_count, - b.index_update_count = iops.index_update_count, - b.index_delete_count = iops.index_delete_count, - b.cx_insert_count = iops.cx_insert_count, - b.cx_update_count = iops.cx_update_count, - b.cx_delete_count = iops.cx_delete_count, - b.table_insert_count = iops.table_insert_count, - b.table_update_count = iops.table_update_count, - b.table_delete_count = iops.table_delete_count -FROM #working_warnings AS b -JOIN iops ON iops.query_hash = b.query_hash -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_spatial = x.is_spatial -FROM #working_warnings AS b -JOIN ( -SELECT r.sql_handle, - 1 AS is_spatial -FROM #relop r -CROSS APPLY r.relop.nodes('/p:RelOp//p:Object') n(fn) -WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_forced_serial = 1 -FROM #query_plan qp -JOIN #working_warnings AS b -ON qp.sql_handle = b.sql_handle -AND b.is_parallel IS NULL -AND qp.query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.columnstore_row_mode = x.is_row_mode -FROM #working_warnings AS b -JOIN ( -SELECT - r.sql_handle, - r.relop.exist('/p:RelOp[(@EstimatedExecutionMode[.="Row"])]') AS is_row_mode -FROM #relop r -WHERE r.relop.exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 -) AS x ON x.sql_handle = b.sql_handle -OPTION (RECOMPILE); - - -RAISERROR('Checking for row level security only', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.is_row_level = 1 -FROM #working_warnings b -JOIN #statements s -ON s.query_hash = b.query_hash -WHERE s.statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 -OPTION (RECOMPILE); - -RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES ( - 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) -, selects -AS ( SELECT s.plan_id, s.query_id - FROM #statements AS s - WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) -, spools -AS ( SELECT DISTINCT r.plan_id, - r.query_id, - c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, - c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, - c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds -FROM #relop AS r -JOIN selects AS s -ON s.plan_id = r.plan_id - AND s.query_id = r.query_id -CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) -WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 -) -UPDATE ww - SET ww.index_spool_rows = sp.estimated_rows, - ww.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM #working_warnings ww -JOIN spools sp -ON ww.plan_id = sp.plan_id -AND ww.query_id = sp.query_id -OPTION ( RECOMPILE ); - - -IF (PARSENAME(CONVERT(VARCHAR(128), SERVERPROPERTY ('PRODUCTVERSION')), 4)) >= 14 - -BEGIN - -RAISERROR(N'Beginning 2017 specfic checks', 0, 1) WITH NOWAIT; - -RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT #stats_agg WITH (TABLOCK) - (sql_handle, last_update, modification_count, sampling_percent, [statistics], [table], [schema], [database]) -SELECT qp.sql_handle, - x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, - x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] -FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) -OPTION (RECOMPILE); - - -RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; -WITH stale_stats AS ( - SELECT sa.sql_handle - FROM #stats_agg AS sa - GROUP BY sa.sql_handle - HAVING MAX(sa.last_update) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.modification_count) >= 100000 -) -UPDATE b -SET b.stale_stats = 1 -FROM #working_warnings AS b -JOIN stale_stats os -ON b.sql_handle = os.sql_handle -OPTION (RECOMPILE); - - -RAISERROR(N'Checking for Adaptive Joins', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -aj AS ( - SELECT r.sql_handle - FROM #relop AS r - CROSS APPLY r.relop.nodes('//p:RelOp') x(c) - WHERE x.c.exist('@IsAdaptive[.=1]') = 1 -) -UPDATE b -SET b.is_adaptive = 1 -FROM #working_warnings AS b -JOIN aj -ON b.sql_handle = aj.sql_handle -OPTION (RECOMPILE); - -END; - - -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - b.unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') -FROM #query_plan qp -JOIN #working_warnings AS b -ON b.query_hash = qp.query_hash -OPTION (RECOMPILE); - - -RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; -;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -, tf_pretty AS ( -SELECT qp.sql_handle, - q.n.value('@Value', 'INT') AS trace_flag, - q.n.value('@Scope', 'VARCHAR(10)') AS scope -FROM #query_plan qp -CROSS APPLY qp.query_plan.nodes('/p:QueryPlan/p:TraceFlags/p:TraceFlag') AS q(n) -) -INSERT #trace_flags WITH (TABLOCK) - (sql_handle, global_trace_flags, session_trace_flags ) -SELECT DISTINCT tf1.sql_handle , - STUFF(( - SELECT DISTINCT N', ' + CONVERT(NVARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.sql_handle = tf2.sql_handle - AND tf2.scope = 'Global' - FOR XML PATH(N'')), 1, 2, N'' - ) AS global_trace_flags, - STUFF(( - SELECT DISTINCT N', ' + CONVERT(NVARCHAR(5), tf2.trace_flag) - FROM tf_pretty AS tf2 - WHERE tf1.sql_handle = tf2.sql_handle - AND tf2.scope = 'Session' - FOR XML PATH(N'')), 1, 2, N'' - ) AS session_trace_flags -FROM tf_pretty AS tf1 -OPTION (RECOMPILE); - -UPDATE b -SET b.trace_flags_session = tf.session_trace_flags -FROM #working_warnings AS b -JOIN #trace_flags tf -ON tf.sql_handle = b.sql_handle -OPTION (RECOMPILE); - -RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), -is_paul_white_electric AS ( -SELECT 1 AS [is_paul_white_electric], -r.sql_handle -FROM #relop AS r -CROSS APPLY r.relop.nodes('//p:RelOp') c(n) -WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 -) -UPDATE b -SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM #working_warnings AS b -JOIN is_paul_white_electric ipwe -ON ipwe.sql_handle = b.sql_handle -OPTION (RECOMPILE); - -IF EXISTS ( SELECT 1 - FROM #working_warnings AS ww - WHERE ww.implicit_conversions = 1 - OR ww.proc_or_function_name <> N'Statement' ) - BEGIN - - RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; - - RAISERROR(N'Getting variable info', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #variable_info ( query_hash, sql_handle, proc_name, variable_name, variable_datatype, compile_time_value ) - SELECT DISTINCT - qp.query_hash, - qp.sql_handle, - b.proc_or_function_name AS proc_name, - q.n.value('@Column', 'NVARCHAR(128)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(128)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(1000)') AS compile_time_value - FROM #query_plan AS qp - JOIN #working_warnings AS b - ON (b.query_hash = qp.query_hash AND b.proc_or_function_name = 'adhoc') - OR (b.sql_handle = qp.sql_handle AND b.proc_or_function_name <> 'adhoc') - CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) - OPTION ( RECOMPILE ); - - RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #conversion_info ( query_hash, sql_handle, proc_name, expression ) - SELECT DISTINCT - qp.query_hash, - qp.sql_handle, - b.proc_or_function_name AS proc_name, - qq.c.value('@Expression', 'NVARCHAR(128)') AS expression - FROM #query_plan AS qp - JOIN #working_warnings AS b - ON (b.query_hash = qp.query_hash AND b.proc_or_function_name = 'adhoc') - OR (b.sql_handle = qp.sql_handle AND b.proc_or_function_name <> 'adhoc') - CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) - WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 - AND b.implicit_conversions = 1 - OPTION ( RECOMPILE ); - - RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( sql_handle, query_hash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) - SELECT ci.sql_handle, - ci.query_hash, - ci.proc_name, - CASE WHEN ci.at_charindex > 0 - AND ci.bracket_charindex > 0 - THEN SUBSTRING(ci.expression, ci.at_charindex, ci.bracket_charindex) - ELSE N'**no_variable**' - END AS variable_name, - N'**no_variable**' AS variable_datatype, - CASE WHEN ci.at_charindex = 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column**' - END AS converted_column_name, - CASE WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex = 0 - THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) - WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 - AND ci.convert_implicit_charindex > 0 - THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) - WHEN ci.at_charindex > 0 - AND ci.comma_charindex > 0 - AND ci.second_comma_charindex > 0 - THEN SUBSTRING(ci.expression, ci.comma_charindex, ci.second_comma_charindex) - ELSE N'**no_column **' - END AS column_name, - CASE WHEN ci.paren_charindex > 0 - 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 - 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 - FROM #conversion_info AS ci - OPTION ( RECOMPILE ); - - IF EXISTS ( SELECT * - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) - OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) - AND sp.variable_name = vi.variable_name ) - BEGIN - RAISERROR(N'Updating variables', 0, 1) WITH NOWAIT; - UPDATE sp - SET sp.variable_datatype = vi.variable_datatype, - sp.compile_time_value = vi.compile_time_value - FROM #stored_proc_info AS sp - JOIN #variable_info AS vi - ON (sp.proc_name = 'adhoc' AND sp.query_hash = vi.query_hash) - OR (sp.proc_name <> 'adhoc' AND sp.sql_handle = vi.sql_handle) - AND sp.variable_name = vi.variable_name - OPTION ( RECOMPILE ); - END - ELSE - BEGIN - RAISERROR(N'Inserting variables', 0, 1) WITH NOWAIT; - INSERT #stored_proc_info ( sql_handle, query_hash, variable_name, variable_datatype, compile_time_value, proc_name ) - SELECT vi.sql_handle, vi.query_hash, vi.variable_name, vi.variable_datatype, vi.compile_time_value, vi.proc_name - FROM #variable_info AS vi - OPTION ( RECOMPILE ); - END - - RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; - UPDATE s - SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) - ELSE s.variable_datatype - END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) - ELSE s.converted_to - END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) - WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' THEN - QUOTENAME(compile_time_value, '''') - ELSE s.compile_time_value - END - FROM #stored_proc_info AS s - OPTION(RECOMPILE); - - RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; - WITH precheck AS ( - SELECT spi.sql_handle, - spi.proc_name, - CONVERT(XML, - N' 'Statement' - THEN N'The stored procedure ' + spi.proc_name - ELSE N'This ad hoc statement' - END - + N' had the following implicit conversions: ' - + CHAR(10) - + STUFF(( - SELECT DISTINCT - @cr + @lf - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN N'The variable ' - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'The compiled value ' - WHEN spi2.column_name LIKE '%Expr%' - THEN 'The expression ' - ELSE N'The column ' - END - + CASE WHEN spi2.variable_name <> N'**no_variable**' - THEN spi2.variable_name - WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN spi2.compile_time_value - - ELSE spi2.column_name - END - + N' has a data type of ' - + CASE WHEN spi2.variable_datatype = N'**no_variable**' THEN spi2.converted_to - ELSE spi2.variable_datatype - END - + N' which caused implicit conversion on the column ' - + CASE WHEN spi2.column_name LIKE N'%CONVERT_IMPLICIT%' - THEN spi2.converted_column_name - WHEN spi2.column_name = N'**no_column**' - THEN spi2.converted_column_name - WHEN spi2.converted_column_name = N'**no_column**' - THEN spi2.column_name - WHEN spi2.column_name <> spi2.converted_column_name - THEN spi2.converted_column_name - ELSE spi2.column_name - END - + CASE WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') - THEN N'' - WHEN spi2.column_name LIKE '%Expr%' - THEN N'' - WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') - THEN ' with the value ' + RTRIM(spi2.compile_time_value) - ELSE N'' - END - + '.' - FROM #stored_proc_info AS spi2 - WHERE spi.sql_handle = spi2.sql_handle - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info - FROM #stored_proc_info AS spi - GROUP BY spi.sql_handle, spi.proc_name - ) - UPDATE b - SET b.implicit_conversion_info = pk.implicit_conversion_info - FROM #working_warnings AS b - JOIN precheck AS pk - ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); - - RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; - WITH precheck AS ( - SELECT spi.sql_handle, - spi.proc_name, - CONVERT(XML, - N' N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' - THEN spi2.variable_name + N' = ' - ELSE @cr + @lf + N' We could not find any cached parameter values for this stored proc. ' - END - + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' - THEN @cr + @lf + N' Possible reasons include declared variables inside the procedure, recompile hints, etc. ' - WHEN spi2.compile_time_value = N'NULL' - THEN spi2.compile_time_value - ELSE RTRIM(spi2.compile_time_value) - END - FROM #stored_proc_info AS spi2 - WHERE spi.sql_handle = spi2.sql_handle - AND spi2.proc_name <> N'Statement' - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @cr + @lf - + N' -- ?>' - ) AS cached_execution_parameters - FROM #stored_proc_info AS spi - GROUP BY spi.sql_handle, spi.proc_name - ) - UPDATE b - SET b.cached_execution_parameters = pk.cached_execution_parameters - FROM #working_warnings AS b - JOIN precheck AS pk - ON pk.sql_handle = b.sql_handle - OPTION ( RECOMPILE ); - - - END; --End implicit conversion information gathering - - -UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN - N'' - ELSE b.implicit_conversion_info - END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN - N'' - ELSE b.cached_execution_parameters - END -FROM #working_warnings AS b -OPTION ( RECOMPILE ); - -/*Begin Missing Index*/ - -IF EXISTS - (SELECT 1 FROM #working_warnings AS ww WHERE ww.missing_index_count > 0 ) - - BEGIN - - RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_xml - SELECT qp.query_hash, - qp.sql_handle, - c.mg.value('@Impact', 'FLOAT') AS Impact, - c.mg.query('.') AS cmg - FROM #query_plan AS qp - CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) - WHERE qp.query_hash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_schema - SELECT mix.query_hash, mix.sql_handle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , - c.mi.query('.') - FROM #missing_index_xml AS mix - CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_usage - SELECT ms.query_hash, ms.sql_handle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, - c.cg.value('@Usage', 'NVARCHAR(128)'), - c.cg.query('.') - FROM #missing_index_schema ms - CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; - WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) - INSERT #missing_index_detail - SELECT miu.query_hash, - miu.sql_handle, - miu.impact, - miu.database_name, - miu.schema_name, - miu.table_name, - miu.usage, - c.c.value('@Name', 'NVARCHAR(128)') - FROM #missing_index_usage AS miu - CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); - - RAISERROR(N'Inserting to #missing_index_pretty', 0, 1) WITH NOWAIT; - INSERT #missing_index_pretty - SELECT m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'EQUALITY' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INEQUALITY' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality - , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name - FROM #missing_index_detail AS m2 - WHERE m2.usage = 'INCLUDE' - AND m.query_hash = m2.query_hash - AND m.sql_handle = m2.sql_handle - AND m.impact = m2.impact - AND m.database_name = m2.database_name - AND m.schema_name = m2.schema_name - AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] - FROM #missing_index_detail AS m - GROUP BY m.query_hash, m.sql_handle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); - - RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; - WITH missing AS ( - SELECT mip.query_hash, - mip.sql_handle, - CONVERT(XML, - N'' - ) AS full_details - FROM #missing_index_pretty AS mip - GROUP BY mip.query_hash, mip.sql_handle, mip.impact - ) - UPDATE ww - SET ww.missing_indexes = m.full_details - FROM #working_warnings AS ww - JOIN missing AS m - ON m.sql_handle = ww.sql_handle - OPTION(RECOMPILE); - - - END - - RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; - UPDATE ww - SET ww.missing_indexes = - CASE WHEN ww.missing_indexes IS NULL - THEN '' - ELSE ww.missing_indexes - END - FROM #working_warnings AS ww - OPTION(RECOMPILE); - -/*End Missing Index*/ - -RAISERROR(N'General query dispositions: frequent executions, long running, etc.', 0, 1) WITH NOWAIT; - -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE b -SET b.frequent_execution = CASE WHEN wm.xpm > @execution_threshold THEN 1 END , - b.near_parallel = CASE WHEN b.query_cost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, - b.long_running = CASE WHEN wm.avg_duration > @long_running_query_warning_seconds THEN 1 - WHEN wm.max_duration > @long_running_query_warning_seconds THEN 1 - WHEN wm.avg_cpu_time > @long_running_query_warning_seconds THEN 1 - WHEN wm.max_cpu_time > @long_running_query_warning_seconds THEN 1 END, - b.is_key_lookup_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.key_lookup_cost >= b.query_cost * .5 THEN 1 END, - b.is_sort_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.sort_cost >= b.query_cost * .5 THEN 1 END, - b.is_remote_query_expensive = CASE WHEN b.remote_query_cost >= b.query_cost * .05 THEN 1 END, - b.is_unused_grant = CASE WHEN percent_memory_grant_used <= @memory_grant_warning_percent AND min_grant_kb > @min_memory_per_query THEN 1 END, - b.long_running_low_cpu = CASE WHEN wm.avg_duration > wm.avg_cpu_time * 4 THEN 1 END, - b.low_cost_high_cpu = CASE WHEN b.query_cost < @ctp AND wm.avg_cpu_time > 500. AND b.query_cost * 10 < wm.avg_cpu_time THEN 1 END, - b.is_spool_expensive = CASE WHEN b.query_cost > (@ctp / 2) AND b.index_spool_cost >= b.query_cost * .1 THEN 1 END, - b.is_spool_more_rows = CASE WHEN b.index_spool_rows >= wm.min_rowcount THEN 1 END, - b.is_bad_estimate = CASE WHEN wm.avg_rowcount > 0 AND (b.estimated_rows * 10000 < wm.avg_rowcount OR b.estimated_rows > wm.avg_rowcount * 10000) THEN 1 END, - b.is_big_log = CASE WHEN wm.avg_log_bytes_used >= (@log_size_mb / 2.) THEN 1 END, - b.is_big_tempdb = CASE WHEN wm.avg_tempdb_space_used >= (@avg_tempdb_data_file / 2.) THEN 1 END -FROM #working_warnings AS b -JOIN #working_metrics AS wm -ON b.plan_id = wm.plan_id -AND b.query_id = wm.query_id -JOIN #working_plan_text AS wpt -ON b.plan_id = wpt.plan_id -AND b.query_id = wpt.query_id -OPTION (RECOMPILE); - - -RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; -/* Populate warnings */ -UPDATE b -SET b.warnings = SUBSTRING( - CASE WHEN b.warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + - CASE WHEN b.compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + - CASE WHEN b.compile_memory_limit_exceeded = 1 THEN ', Compile Memory Limit Exceeded' ELSE '' END + - CASE WHEN b.is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + - CASE WHEN b.is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + - CASE WHEN b.unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN b.missing_index_count > 0 THEN ', Missing Indexes (' + CAST(b.missing_index_count AS NVARCHAR(3)) + ')' ELSE '' END + - CASE WHEN b.unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(b.unmatched_index_count AS NVARCHAR(3)) + ')' ELSE '' END + - CASE WHEN b.is_cursor = 1 THEN ', Cursor' - + CASE WHEN b.is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN b.is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END - ELSE '' END + - CASE WHEN b.is_parallel = 1 THEN ', Parallel' ELSE '' END + - CASE WHEN b.near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + - CASE WHEN b.frequent_execution = 1 THEN ', Frequent Execution' ELSE '' END + - CASE WHEN b.plan_warnings = 1 THEN ', Plan Warnings' ELSE '' END + - CASE WHEN b.parameter_sniffing = 1 THEN ', Parameter Sniffing' ELSE '' END + - CASE WHEN b.long_running = 1 THEN ', Long Running Query' ELSE '' END + - CASE WHEN b.downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + - CASE WHEN b.implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + - CASE WHEN b.plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + - CASE WHEN b.is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + - CASE WHEN b.is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + - CASE WHEN b.is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + - CASE WHEN b.is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + - CASE WHEN b.trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + b.trace_flags_session ELSE '' END + - CASE WHEN b.is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN b.function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), b.function_count) + ' function(s)' ELSE '' END + - CASE WHEN b.clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), b.clr_function_count) + ' CLR function(s)' ELSE '' END + - CASE WHEN b.is_table_variable = 1 THEN ', Table Variables' ELSE '' END + - CASE WHEN b.no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + - CASE WHEN b.relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN b.is_table_scan = 1 THEN ', Table Scans' ELSE '' END + - CASE WHEN b.backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + - CASE WHEN b.forced_index = 1 THEN ', Forced Indexes' ELSE '' END + - CASE WHEN b.forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + - CASE WHEN b.forced_scan = 1 THEN ', Forced Scans' ELSE '' END + - CASE WHEN b.columnstore_row_mode = 1 THEN ', ColumnStore Row Mode ' ELSE '' END + - CASE WHEN b.is_computed_scalar = 1 THEN ', Computed Column UDF ' ELSE '' END + - CASE WHEN b.is_sort_expensive = 1 THEN ', Expensive Sort' ELSE '' END + - CASE WHEN b.is_computed_filter = 1 THEN ', Filter UDF' ELSE '' END + - CASE WHEN b.index_ops >= 5 THEN ', >= 5 Indexes Modified' ELSE '' END + - CASE WHEN b.is_row_level = 1 THEN ', Row Level Security' ELSE '' END + - CASE WHEN b.is_spatial = 1 THEN ', Spatial Index' ELSE '' END + - CASE WHEN b.index_dml = 1 THEN ', Index DML' ELSE '' END + - CASE WHEN b.table_dml = 1 THEN ', Table DML' ELSE '' END + - CASE WHEN b.low_cost_high_cpu = 1 THEN ', Low Cost High CPU' ELSE '' END + - CASE WHEN b.long_running_low_cpu = 1 THEN + ', Long Running With Low CPU' ELSE '' END + - CASE WHEN b.stale_stats = 1 THEN + ', Statistics used have > 100k modifications in the last 7 days' ELSE '' END + - CASE WHEN b.is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + - CASE WHEN b.is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + - CASE WHEN b.is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN b.is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN b.is_big_log = 1 THEN + ', High log use' ELSE '' END + - CASE WHEN b.is_big_tempdb = 1 THEN ', High tempdb use' ELSE '' END + - CASE WHEN b.is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM #working_warnings b -OPTION (RECOMPILE); - - -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure generating warnings.', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - - -BEGIN TRY -BEGIN - -RAISERROR(N'Checking for parameter sniffing symptoms', 0, 1) WITH NOWAIT; - -UPDATE b -SET b.parameter_sniffing_symptoms = - SUBSTRING( - /*Duration*/ - CASE WHEN (b.min_duration * 100) < (b.avg_duration) THEN ', Fast sometimes' ELSE '' END + - CASE WHEN (b.max_duration) > (b.avg_duration * 100) THEN ', Slow sometimes' ELSE '' END + - CASE WHEN (b.last_duration * 100) < (b.avg_duration) THEN ', Fast last run' ELSE '' END + - CASE WHEN (b.last_duration) > (b.avg_duration * 100) THEN ', Slow last run' ELSE '' END + - /*CPU*/ - CASE WHEN (b.min_cpu_time / b.avg_dop) * 100 < (b.avg_cpu_time / b.avg_dop) THEN ', Low CPU sometimes' ELSE '' END + - CASE WHEN (b.max_cpu_time / b.max_dop) > (b.avg_cpu_time / b.avg_dop) * 100 THEN ', High CPU sometimes' ELSE '' END + - CASE WHEN (b.last_cpu_time / b.last_dop) * 100 < (b.avg_cpu_time / b.avg_dop) THEN ', Low CPU last run' ELSE '' END + - CASE WHEN (b.last_cpu_time / b.last_dop) > (b.avg_cpu_time / b.avg_dop) * 100 THEN ', High CPU last run' ELSE '' END + - /*Logical Reads*/ - CASE WHEN (b.min_logical_io_reads * 100) < (b.avg_logical_io_reads) THEN ', Low reads sometimes' ELSE '' END + - CASE WHEN (b.max_logical_io_reads) > (b.avg_logical_io_reads * 100) THEN ', High reads sometimes' ELSE '' END + - CASE WHEN (b.last_logical_io_reads * 100) < (b.avg_logical_io_reads) THEN ', Low reads last run' ELSE '' END + - CASE WHEN (b.last_logical_io_reads) > (b.avg_logical_io_reads * 100) THEN ', High reads last run' ELSE '' END + - /*Logical Writes*/ - CASE WHEN (b.min_logical_io_writes * 100) < (b.avg_logical_io_writes) THEN ', Low writes sometimes' ELSE '' END + - CASE WHEN (b.max_logical_io_writes) > (b.avg_logical_io_writes * 100) THEN ', High writes sometimes' ELSE '' END + - CASE WHEN (b.last_logical_io_writes * 100) < (b.avg_logical_io_writes) THEN ', Low writes last run' ELSE '' END + - CASE WHEN (b.last_logical_io_writes) > (b.avg_logical_io_writes * 100) THEN ', High writes last run' ELSE '' END + - /*Physical Reads*/ - CASE WHEN (b.min_physical_io_reads * 100) < (b.avg_physical_io_reads) THEN ', Low physical reads sometimes' ELSE '' END + - CASE WHEN (b.max_physical_io_reads) > (b.avg_physical_io_reads * 100) THEN ', High physical reads sometimes' ELSE '' END + - CASE WHEN (b.last_physical_io_reads * 100) < (b.avg_physical_io_reads) THEN ', Low physical reads last run' ELSE '' END + - CASE WHEN (b.last_physical_io_reads) > (b.avg_physical_io_reads * 100) THEN ', High physical reads last run' ELSE '' END + - /*Memory*/ - CASE WHEN (b.min_query_max_used_memory * 100) < (b.avg_query_max_used_memory) THEN ', Low memory sometimes' ELSE '' END + - CASE WHEN (b.max_query_max_used_memory) > (b.avg_query_max_used_memory * 100) THEN ', High memory sometimes' ELSE '' END + - CASE WHEN (b.last_query_max_used_memory * 100) < (b.avg_query_max_used_memory) THEN ', Low memory last run' ELSE '' END + - CASE WHEN (b.last_query_max_used_memory) > (b.avg_query_max_used_memory * 100) THEN ', High memory last run' ELSE '' END + - /*Duration*/ - CASE WHEN b.min_rowcount * 100 < b.avg_rowcount THEN ', Low row count sometimes' ELSE '' END + - CASE WHEN b.max_rowcount > b.avg_rowcount * 100 THEN ', High row count sometimes' ELSE '' END + - CASE WHEN b.last_rowcount * 100 < b.avg_rowcount THEN ', Low row count run' ELSE '' END + - CASE WHEN b.last_rowcount > b.avg_rowcount * 100 THEN ', High row count last run' ELSE '' END + - /*DOP*/ - CASE WHEN b.min_dop = 1 THEN ', Serial sometimes' ELSE '' END + - CASE WHEN b.max_dop > 1 THEN ', Parallel sometimes' ELSE '' END + - CASE WHEN b.last_dop = 1 THEN ', Serial last run' ELSE '' END + - CASE WHEN b.last_dop > 1 THEN ', Parallel last run' ELSE '' END + - /*tempdb*/ - CASE WHEN b.min_tempdb_space_used * 100 < b.avg_tempdb_space_used THEN ', Low tempdb sometimes' ELSE '' END + - CASE WHEN b.max_tempdb_space_used > b.avg_tempdb_space_used * 100 THEN ', High tempdb sometimes' ELSE '' END + - CASE WHEN b.last_tempdb_space_used * 100 < b.avg_tempdb_space_used THEN ', Low tempdb run' ELSE '' END + - CASE WHEN b.last_tempdb_space_used > b.avg_tempdb_space_used * 100 THEN ', High tempdb last run' ELSE '' END + - /*tlog*/ - CASE WHEN b.min_log_bytes_used * 100 < b.avg_log_bytes_used THEN ', Low log use sometimes' ELSE '' END + - CASE WHEN b.max_log_bytes_used > b.avg_log_bytes_used * 100 THEN ', High log use sometimes' ELSE '' END + - CASE WHEN b.last_log_bytes_used * 100 < b.avg_log_bytes_used THEN ', Low log use run' ELSE '' END + - CASE WHEN b.last_log_bytes_used > b.avg_log_bytes_used * 100 THEN ', High log use last run' ELSE '' END - , 2, 200000) -FROM #working_metrics AS b -OPTION (RECOMPILE); - -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure analyzing parameter sniffing', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -BEGIN TRY - -BEGIN - -IF (@Failed = 0 AND @ExportToExcel = 0 AND @SkipXML = 0) -BEGIN - -RAISERROR(N'Returning regular results', 0, 1) WITH NOWAIT; - - -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.last_force_failure_reason_desc, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); - -END; - -IF (@Failed = 1 AND @ExportToExcel = 0 AND @SkipXML = 0) -BEGIN - -RAISERROR(N'Returning results for failed queries', 0, 1) WITH NOWAIT; - -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, wpt.query_plan_xml, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, ww.missing_indexes, ww.implicit_conversion_info, ww.cached_execution_parameters, - wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); - -END; - -IF (@ExportToExcel = 1 AND @SkipXML = 0) -BEGIN - -RAISERROR(N'Returning results for Excel export', 0, 1) WITH NOWAIT; - -UPDATE #working_plan_text -SET query_sql_text = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(query_sql_text)),' ','<>'),'><',''),'<>',' '), 1, 31000) -OPTION (RECOMPILE); - -WITH x AS ( -SELECT wpt.database_name, ww.query_cost, wm.plan_id, wm.query_id, wpt.query_sql_text, wm.proc_or_function_name, ww.warnings, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.last_force_failure_reason_desc, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_warnings AS ww - ON wpt.plan_id = ww.plan_id - AND wpt.query_id = ww.query_id -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); - -END; - -IF (@ExportToExcel = 0 AND @SkipXML = 1) -BEGIN - -RAISERROR(N'Returning results for skipped XML', 0, 1) WITH NOWAIT; - -WITH x AS ( -SELECT wpt.database_name, wm.plan_id, wm.query_id, wpt.query_sql_text, wpt.query_plan_xml, wpt.pattern, - wm.parameter_sniffing_symptoms, wpt.top_three_waits, wm.count_executions, wm.count_compiles, wm.total_cpu_time, wm.avg_cpu_time, - wm.total_duration, wm.avg_duration, wm.total_logical_io_reads, wm.avg_logical_io_reads, - wm.total_physical_io_reads, wm.avg_physical_io_reads, wm.total_logical_io_writes, wm.avg_logical_io_writes, wm.total_rowcount, wm.avg_rowcount, - wm.total_query_max_used_memory, wm.avg_query_max_used_memory, wm.total_tempdb_space_used, wm.avg_tempdb_space_used, - wm.total_log_bytes_used, wm.avg_log_bytes_used, wm.total_num_physical_io_reads, wm.avg_num_physical_io_reads, - wm.first_execution_time, wm.last_execution_time, wpt.last_force_failure_reason_desc, wpt.context_settings, ROW_NUMBER() OVER (PARTITION BY wm.plan_id, wm.query_id, wm.last_execution_time ORDER BY wm.plan_id) AS rn -FROM #working_plan_text AS wpt -JOIN #working_metrics AS wm - ON wpt.plan_id = wm.plan_id - AND wpt.query_id = wm.query_id -) -SELECT * -FROM x -WHERE x.rn = 1 -ORDER BY x.last_execution_time -OPTION (RECOMPILE); - -END; - -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning results', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -BEGIN TRY -BEGIN - -IF (@ExportToExcel = 0 AND @HideSummary = 0 AND @SkipXML = 0) -BEGIN - RAISERROR('Building query plan summary data.', 0, 1) WITH NOWAIT; - - /* Build summary data */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE frequent_execution = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1, - 100, - 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', - 'Queries are being executed more than ' - + CAST (@execution_threshold AS VARCHAR(5)) - + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE parameter_sniffing = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 2, - 50, - 'Parameterization', - 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', - 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; - - /* Forced execution plans */ - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_forced_plan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 3, - 5, - 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', - 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 4, - 200, - 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_forced_parameterized = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 5, - 50, - 'Parameterization', - 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', - 'Execution plans have been compiled with forced parameterization.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_parallel = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 6, - 200, - 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', - 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.near_parallel = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 7, - 200, - 'Execution Plans', - 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', - 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.plan_warnings = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 8, - 50, - 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.long_running = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 9, - 50, - 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', - 'Long running queries have been found. These are queries with an average duration longer than ' - + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) - + ' second(s). These queries should be investigated for additional tuning options.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.missing_index_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 10, - 50, - 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', - 'Queries found with missing indexes.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.downlevel_estimator = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 13, - 200, - 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', - 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.implicit_conversions = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 14, - 50, - 'Performance', - 'Implicit Conversions', - 'http://brentozar.com/go/implicit', - 'One or more queries are comparing two fields that are not of the same data type.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE compile_timeout = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 18, - 50, - 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', - 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE compile_memory_limit_exceeded = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 19, - 50, - 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', - 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE warning_no_join_predicate = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 20, - 10, - 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', - 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE plan_multiple_plans = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 21, - 200, - 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', - 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE unmatched_index_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 22, - 100, - 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', - 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE unparameterized_query = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 23, - 100, - 'Parameterization', - 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', - 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings - WHERE is_trivial = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 24, - 100, - 'Execution Plans', - 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', - 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_forced_serial= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 25, - 10, - 'Execution Plans', - 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', - 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_key_lookup_expensive= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 26, - 100, - 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', - 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_remote_query_expensive= 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 28, - 100, - 'Execution Plans', - 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', - 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.trace_flags_session IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 29, - 100, - 'Trace Flags', - 'Session Level Trace Flags Enabled', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'Someone is enabling session level Trace Flags in a query.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_unused_grant IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 30, - 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', - 'https://www.brentozar.com/blitzcache/unused-memory-grants/', - 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.function_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 31, - 100, - 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.clr_function_count > 0 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 32, - 100, - 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; - - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_table_variable = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 33, - 100, - 'Table Variables detected', - 'Beware nasty side effects', - 'https://www.brentozar.com/blitzcache/table-variables/', - 'All modifications are single threaded, and selects have really low row estimates.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.no_stats_warning = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 35, - 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', - 'https://www.brentozar.com/blitzcache/columns-no-statistics/', - 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.relop_warnings = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 36, - 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', - 'Check the plan for more details.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_table_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 37, - 100, - 'Table Scans', - 'Your database has HEAPs', - 'https://www.brentozar.com/archive/2012/05/video-heaps/', - 'This may not be a problem. Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.backwards_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 38, - 100, - 'Backwards Scans', - 'Indexes are being read backwards', - 'https://www.brentozar.com/blitzcache/backwards-scans/', - 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.forced_index = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 39, - 100, - 'Index forcing', - 'Someone is using hints to force index usage', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans, and will prevent missing index requests.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.forced_seek = 1 - OR p.forced_scan = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 40, - 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', - 'https://www.brentozar.com/blitzcache/optimizer-forcing/', - 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.columnstore_row_mode = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 41, - 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', - 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', - 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_computed_scalar = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 42, - 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', - 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', - 'This can cause a whole mess of bad serializartion problems.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_sort_expensive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 43, - 100, - 'Execution Plans', - 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', - 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_computed_filter = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 44, - 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', - 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'Someone put a Scalar UDF in the WHERE clause!') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.index_ops >= 5 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 45, - 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', - 'No URL yet', - 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_row_level = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', - 'No URL yet', - 'You may see a lot of confusing junk in your query plan.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spatial = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 47, - 200, - 'Spatial Abuse', - 'You hit a Spatial Index', - 'No URL yet', - 'Purely informational.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.index_dml = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 48, - 150, - 'Index DML', - 'Indexes were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.table_dml = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 49, - 150, - 'Table DML', - 'Tables were created or dropped', - 'No URL yet', - 'This can cause recompiles and stuff.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.long_running_low_cpu = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 50, - 150, - 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', - 'No URL yet', - 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.low_cost_high_cpu = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 51, - 150, - 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', - 'No URL yet', - 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.stale_stats = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 52, - 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', - 'No URL yet', - 'Ever heard of updating statistics?') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_adaptive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', - 'No URL yet', - 'Joe Sack rules.') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spool_expensive = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 54, - 150, - 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_spool_more_rows = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 55, - 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', - 'No URL yet', - 'Check operator predicates and output for index definition guidance') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_bad_estimate = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 56, - 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', - 'No URL yet', - 'This may indicate a performance problem if mismatches occur regularly') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_big_log = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 57, - 100, - 'High transaction log use', - 'This query on average uses more than half of the transaction log', - 'michaeljswart.com/2014/09/take-care-when-scripting-batches/', - 'This is probably a sign that you need to start batching queries') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_big_tempdb = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 58, - 100, - 'High tempdb use', - 'This query uses more than half of a data file on average', - 'No URL yet', - 'You should take a look at tempdb waits to see if you''re having problems') ; - - IF EXISTS (SELECT 1/0 - FROM #working_warnings p - WHERE p.is_paul_white_electric = 1 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 998, - 200, - 'Is Paul White Electric?', - 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', - 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT - 999, - 200, - 'Database Level Statistics', - 'The database ' + sa.[database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.last_update))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.modification_count)) + ' modifications on average.' AS Finding, - '' AS URL, - 'Consider updating statistics more frequently,' AS Details - FROM #stats_agg AS sa - GROUP BY sa.[database] - HAVING MAX(sa.last_update) <= DATEADD(DAY, -7, SYSDATETIME()) - AND AVG(sa.modification_count) >= 100000; - - - IF EXISTS (SELECT 1/0 - FROM #trace_flags AS tf - WHERE tf.global_trace_flags IS NOT NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1000, - 255, - 'Global Trace Flags Enabled', - 'You have Global Trace Flags enabled on your server', - 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', - 'You have the following Global Trace Flags enabled: ' + (SELECT TOP 1 tf.global_trace_flags FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL)) ; - - IF EXISTS (SELECT 1/0 - FROM #working_plan_text AS p - WHERE p.min_grant_kb IS NULL - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 1001, - 255, - 'Plans not in cache', - 'We checked sys.dm_exec_query_stats for memory grant info', - '', - 'Plans in Query Store aren''t in other DMVs, which means we can''t get some information about them.') ; - - /* - Return worsts - */ - WITH worsts AS ( - SELECT gi.flat_date, - gi.start_range, - gi.end_range, - gi.total_avg_duration_ms, - gi.total_avg_cpu_time_ms, - gi.total_avg_logical_io_reads_mb, - gi.total_avg_physical_io_reads_mb, - gi.total_avg_logical_io_writes_mb, - gi.total_avg_query_max_used_memory_mb, - gi.total_rowcount, - gi.total_avg_log_bytes_mb, - gi.total_avg_tempdb_space, - CONVERT(NVARCHAR(20), gi.flat_date) AS worst_date, - CASE WHEN DATEPART(HOUR, gi.start_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.start_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range)) + 'am ' - WHEN DATEPART(HOUR, gi.start_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.start_range) -12) + 'pm ' - END AS worst_start_time, - CASE WHEN DATEPART(HOUR, gi.end_range) = 0 THEN ' midnight ' - WHEN DATEPART(HOUR, gi.end_range) <= 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range)) + 'am ' - WHEN DATEPART(HOUR, gi.end_range) > 12 THEN CONVERT(NVARCHAR(3), DATEPART(HOUR, gi.end_range) -12) + 'pm ' - END AS worst_end_time - FROM #grouped_interval AS gi - ), - duration_worst AS ( - SELECT TOP 1 'Your worst duration range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_duration_ms DESC - ), - cpu_worst AS ( - SELECT TOP 1 'Your worst cpu range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_cpu_time_ms DESC - ), - logical_reads_worst AS ( - SELECT TOP 1 'Your worst logical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_logical_io_reads_mb DESC - ), - physical_reads_worst AS ( - SELECT TOP 1 'Your worst physical read range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_physical_io_reads_mb DESC - ), - logical_writes_worst AS ( - SELECT TOP 1 'Your worst logical write range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_logical_io_writes_mb DESC - ), - memory_worst AS ( - SELECT TOP 1 'Your worst memory range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_query_max_used_memory_mb DESC - ), - rowcount_worst AS ( - SELECT TOP 1 'Your worst row count range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_rowcount DESC - ), - logbytes_worst AS ( - SELECT TOP 1 'Your worst log bytes range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_log_bytes_mb DESC - ), - tempdb_worst AS ( - SELECT TOP 1 'Your worst tempdb range was on ' + worsts.worst_date + ' between ' + worsts.worst_start_time + ' and ' + worsts.worst_end_time + '.' AS msg - FROM worsts - ORDER BY worsts.total_avg_tempdb_space DESC - ) - INSERT #warning_results ( CheckID, Priority, FindingsGroup, Finding, URL, Details ) - SELECT 1002, 255, 'Worsts', 'Worst Duration', 'N/A', duration_worst.msg - FROM duration_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst CPU', 'N/A', cpu_worst.msg - FROM cpu_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Reads', 'N/A', logical_reads_worst.msg - FROM logical_reads_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Physical Reads', 'N/A', physical_reads_worst.msg - FROM physical_reads_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Logical Writes', 'N/A', logical_writes_worst.msg - FROM logical_writes_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Memory', 'N/A', memory_worst.msg - FROM memory_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Row Counts', 'N/A', rowcount_worst.msg - FROM rowcount_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst Log Bytes', 'N/A', logbytes_worst.msg - FROM logbytes_worst - UNION ALL - SELECT 1002, 255, 'Worsts', 'Worst tempdb', 'N/A', tempdb_worst.msg - FROM tempdb_worst - OPTION (RECOMPILE); - - - IF NOT EXISTS (SELECT 1/0 - FROM #warning_results AS bcr - WHERE bcr.Priority = 2147483646 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (2147483646, - 255, - 'Need more help?' , - 'Paste your plan on the internet!', - 'http://pastetheplan.com', - 'This makes it easy to share plans and post them to Q&A sites like https://dba.stackexchange.com/!') ; - - - IF NOT EXISTS (SELECT 1/0 - FROM #warning_results AS bcr - WHERE bcr.Priority = 2147483647 - ) - INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES ( - 2147483647, - 255, - 'Thanks for using sp_BlitzQueryStore!' , - 'From Your Community Volunteers', - 'http://FirstResponderKit.org', - 'We hope you found this tool useful. Current version: ' + @Version + ' released on ' + CONVERT(NVARCHAR(30), @VersionDate) + '.') ; - - - - SELECT Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - FROM #warning_results - GROUP BY Priority, - FindingsGroup, - Finding, - URL, - Details, - CheckID - ORDER BY Priority ASC, CheckID ASC - OPTION (RECOMPILE); - - - -END; - -END; -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning warnings', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -IF @Debug = 1 - -BEGIN TRY - -BEGIN - -RAISERROR(N'Returning debugging data from temp tables', 0, 1) WITH NOWAIT; - ---Table content debugging - -SELECT '#working_metrics' AS table_name, * -FROM #working_metrics AS wm -OPTION (RECOMPILE); - -SELECT '#working_plan_text' AS table_name, * -FROM #working_plan_text AS wpt -OPTION (RECOMPILE); - -SELECT '#working_warnings' AS table_name, * -FROM #working_warnings AS ww -OPTION (RECOMPILE); - -SELECT '#working_wait_stats' AS table_name, * -FROM #working_wait_stats wws -OPTION (RECOMPILE); - -SELECT '#grouped_interval' AS table_name, * -FROM #grouped_interval -OPTION (RECOMPILE); - -SELECT '#working_plans' AS table_name, * -FROM #working_plans -OPTION (RECOMPILE); - -SELECT '#stats_agg' AS table_name, * -FROM #stats_agg -OPTION (RECOMPILE); - -SELECT '#trace_flags' AS table_name, * -FROM #trace_flags -OPTION (RECOMPILE); - -SELECT '#statements' AS table_name, * -FROM #statements AS s -OPTION (RECOMPILE); - -SELECT '#query_plan' AS table_name, * -FROM #query_plan AS qp -OPTION (RECOMPILE); - -SELECT '#relop' AS table_name, * -FROM #relop AS r -OPTION (RECOMPILE); - -SELECT '#plan_cost' AS table_name, * -FROM #plan_cost AS pc -OPTION (RECOMPILE); - -SELECT '#est_rows' AS table_name, * -FROM #est_rows AS er -OPTION (RECOMPILE); - -SELECT '#stored_proc_info' AS table_name, * -FROM #stored_proc_info AS spi -OPTION(RECOMPILE); - -SELECT '#conversion_info' AS table_name, * -FROM #conversion_info AS ci -OPTION ( RECOMPILE ); - -SELECT '#variable_info' AS table_name, * -FROM #variable_info AS vi -OPTION ( RECOMPILE ); - -SELECT '#missing_index_xml' AS table_name, * -FROM #missing_index_xml -OPTION ( RECOMPILE ); - -SELECT '#missing_index_schema' AS table_name, * -FROM #missing_index_schema -OPTION ( RECOMPILE ); - -SELECT '#missing_index_usage' AS table_name, * -FROM #missing_index_usage -OPTION ( RECOMPILE ); - -SELECT '#missing_index_detail' AS table_name, * -FROM #missing_index_detail -OPTION ( RECOMPILE ); - -SELECT '#missing_index_pretty' AS table_name, * -FROM #missing_index_pretty -OPTION ( RECOMPILE ); - -END; - -END TRY -BEGIN CATCH - RAISERROR (N'Failure returning debug temp tables', 0,1) WITH NOWAIT; - - IF @sql_select IS NOT NULL - BEGIN - SET @msg = N'Last @sql_select: ' + @sql_select; - RAISERROR(@msg, 0, 1) WITH NOWAIT; - END; - - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); - RAISERROR (@msg, @error_severity, @error_state) WITH NOWAIT; - - - WHILE @@TRANCOUNT > 0 - ROLLBACK; - - RETURN; -END CATCH; - -/* -Ways to run this thing - ---Debug -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Debug = 1 - ---Get the top 1 -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @Debug = 1 - ---Use a StartDate -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StartDate = '20170527' - ---Use an EndDate -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @EndDate = '20170527' - ---Use Both -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StartDate = '20170526', @EndDate = '20170527' - ---Set a minimum execution count -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @MinimumExecutionCount = 10 - ---Set a duration minimum -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @DurationFilter = 5 - ---Look for a stored procedure name (that doesn't exist!) -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'blah' - ---Look for a stored procedure name that does (at least On My Computer®) -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @StoredProcName = 'UserReportExtended' - ---Look for failed queries -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @Top = 1, @Failed = 1 - ---Filter by plan_id -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @PlanIdFilter = 3356 - ---Filter by query_id -EXEC sp_BlitzQueryStore @DatabaseName = 'StackOverflow', @QueryIdFilter = 2958 - -*/ - -END; - -GO -IF OBJECT_ID('dbo.sp_BlitzWho') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzWho AS RETURN 0;') -GO - -ALTER PROCEDURE dbo.sp_BlitzWho - @Help TINYINT = 0 , - @ShowSleepingSPIDs TINYINT = 0, - @ExpertMode BIT = 0, - @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT -AS -BEGIN - SET NOCOUNT ON; - SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; - - - IF @Help = 1 - PRINT ' -sp_BlitzWho from http://FirstResponderKit.org - -This script gives you a snapshot of everything currently executing on your SQL Server. - -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. - -Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - -MIT License - -Copyright (c) 2017 Brent Ozar Unlimited - -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. -'; - -/* Get the major and minor build numbers */ -DECLARE @ProductVersion NVARCHAR(128) - ,@ProductVersionMajor DECIMAL(10,2) - ,@ProductVersionMinor DECIMAL(10,2) - ,@EnhanceFlag BIT = 0 - ,@StringToExecute NVARCHAR(MAX) - ,@EnhanceSQL NVARCHAR(MAX) = - N'query_stats.last_dop, - query_stats.min_dop, - query_stats.max_dop, - query_stats.last_grant_kb, - query_stats.min_grant_kb, - query_stats.max_grant_kb, - query_stats.last_used_grant_kb, - query_stats.min_used_grant_kb, - query_stats.max_used_grant_kb, - query_stats.last_ideal_grant_kb, - query_stats.min_ideal_grant_kb, - query_stats.max_ideal_grant_kb, - query_stats.last_reserved_threads, - query_stats.min_reserved_threads, - query_stats.max_reserved_threads, - query_stats.last_used_threads, - query_stats.min_used_threads, - query_stats.max_used_threads,' - ,@SessionWaits BIT = 0 - ,@SessionWaitsSQL NVARCHAR(MAX) = - N'LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT TOP 5 waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_time_ms) AS NVARCHAR(128)) - + N'' ms), '' - FROM sys.dm_exec_session_wait_stats AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - HAVING SUM(waitwait.wait_time_ms) > 5 - ORDER BY SUM(waitwait.wait_time_ms) DESC - FOR - XML PATH('''') ) AS session_wait_info - FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 - ON s.session_id = wt2.session_id - LEFT JOIN sys.dm_exec_query_stats AS session_stats - ON r.sql_handle = session_stats.sql_handle - AND r.plan_handle = session_stats.plan_handle - AND r.statement_start_offset = session_stats.statement_start_offset - AND r.statement_end_offset = session_stats.statement_end_offset' - ,@QueryStatsXML BIT = 0 - ,@QueryStatsXMLselect NVARCHAR(MAX) = N' CAST(COALESCE(qs_live.query_plan, '''') AS XML) AS live_query_plan , ' - ,@QueryStatsXMLSQL NVARCHAR(MAX) = N'OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live' - - -SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); -SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1,CHARINDEX('.', @ProductVersion) + 1 ), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) -IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan') - SET @QueryStatsXML = 1; - - -IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 -BEGIN -SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan , - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), - blocked.waittime) + '')'' ) AS wait_info , - CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name , - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' -END -IF @ProductVersionMajor >= 11 -BEGIN -SELECT @EnhanceFlag = - CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 - WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 - WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 - ELSE 0 - END - - -IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL -BEGIN - SET @SessionWaits = 1 -END - - - -SELECT @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; - - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan ,' - + - CASE @QueryStatsXML - WHEN 1 THEN + @QueryStatsXMLselect - ELSE N'' - END - +' - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) AS wait_info ,' - + - CASE @SessionWaits - WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' - ELSE N'' - END - + - N'CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , ' - + - CASE @EnhanceFlag - WHEN 1 THEN @EnhanceSQL - ELSE N'' - END - + - N' - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name, - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END - - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - ' - + - CASE @SessionWaits - WHEN 1 THEN @SessionWaitsSQL - ELSE N'' - END - + - ' - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - ' - + - CASE @QueryStatsXML - WHEN 1 THEN @QueryStatsXMLSQL - ELSE N'' - END - + - ' - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' - -END - -IF @Debug = 1 - BEGIN - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 160000)) - END - -EXEC(@StringToExecute); - -END -GO diff --git a/LICENSE.md b/LICENSE.md index d83cac040..a6a54d558 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,35 +1,42 @@ -MIT License - -Copyright for portions of sp_Blitz are held by Microsoft as part of project -tigertoolbox and are provided under the MIT license: -https://github.com/Microsoft/tigertoolbox -All other copyrights for sp_Blitz are held by Brent Ozar Unlimited, 2017 as -described below. - -Copyright for portions of DatabaseRestore are held by GregWhiteDBA as part -of project MSSQLAutoRestore and are provided under the MIT license: -https://github.com/GregWhiteDBA/MSSQLAutoRestore -All other copyrights for DatabaseRestore are held by Brent Ozar Unlimited, 2017 -as described below. - - - -Copyright (c) 2017 Brent Ozar Unlimited - -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 +MIT License + +Copyright for portions of sp_Blitz are held by Microsoft as part of project +tigertoolbox and are provided under the MIT license: +https://github.com/Microsoft/tigertoolbox +All other copyrights for sp_Blitz are held by Brent Ozar Unlimited, 2022 as +described below. + +Copyright for portions of DatabaseRestore are held by GregWhiteDBA as part +of project MSSQLAutoRestore and are provided under the MIT license: +https://github.com/GregWhiteDBA/MSSQLAutoRestore +All other copyrights for DatabaseRestore are held by Brent Ozar Unlimited, 2022 +as described below. + +Copyright for sp_BlitzInMemoryOLTP are held by Ned Otter and Konstantin +Taranov as part of project sqlserver-kit and are provided under the MIT license: +https://github.com/ktaranov/sqlserver-kit + +Copyright for SqlServerVersionScript are held by Josh Darnell as part of +project SqlServerVersionScript and are provided under the MIT license: +https://github.com/jadarnel27/SqlServerVersionScript + + +Copyright (c) 2022 Brent Ozar Unlimited + +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. \ No newline at end of file diff --git a/Power BI/FirstResponderKit.pbit b/Power BI/FirstResponderKit.pbit deleted file mode 100755 index 8d5d74c47..000000000 Binary files a/Power BI/FirstResponderKit.pbit and /dev/null differ diff --git a/Power BI/FirstResponderKit.pbix b/Power BI/FirstResponderKit.pbix deleted file mode 100755 index 19663b51b..000000000 Binary files a/Power BI/FirstResponderKit.pbix and /dev/null differ diff --git a/README.md b/README.md index 977d0e498..9490300eb 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,29 @@ [![stars badge]][stars] [![forks badge]][forks] [![issues badge]][issues] +[![contributors_badge]][contributors] Navigation + - [How to Install the Scripts](#how-to-install-the-scripts) - [How to Get Support](#how-to-get-support) - - [sp_Blitz: Overall Health Check](#sp_blitz-overall-health-check) - - [Advanced sp_Blitz Parameters](#advanced-sp_blitz-parameters) - - [Writing sp_Blitz Output to a Table](#writing-sp_blitz-output-to-a-table) - - [Skipping Checks or Databases](#skipping-checks-or-databases) - - [sp_BlitzCache: Find the Most Resource-Intensive Queries](#sp_blitzcache-find-the-most-resource-intensive-queries) - - [Advanced sp_BlitzCache Parameters](#advanced-sp_blitzcache-parameters) - - [sp_BlitzIndex: Tune Your Indexes](#sp_blitzindex-tune-your-indexes) - - [Advanced sp_BlitzIndex Parameters](#advanced-sp_blitzindex-parameters) - - [sp_BlitzFirst: Real-Time Performance Advice](#sp_blitzfirst-real-time-performance-advice) - - [sp_BlitzWho: What Queries are Running Now](#sp_blitzwho-what-queries-are-running-now) - - [sp_BlitzQueryStore: Like BlitzCache, for Query Store](#sp_blitzquerystore-query-store-sale) - - [sp_BlitzLock: Deadlock Analysis](#sp_blitzlock-deadlock-analysis) + - Common Scripts: + - [sp_Blitz: Overall Health Check](#sp_blitz-overall-health-check) + - [Advanced sp_Blitz Parameters](#advanced-sp_blitz-parameters) + - [Writing sp_Blitz Output to a Table](#writing-sp_blitz-output-to-a-table) + - [Skipping Checks or Databases](#skipping-checks-or-databases) + - [sp_BlitzCache: Find the Most Resource-Intensive Queries](#sp_blitzcache-find-the-most-resource-intensive-queries) + - [Advanced sp_BlitzCache Parameters](#advanced-sp_blitzcache-parameters) + - [sp_BlitzFirst: Real-Time Performance Advice](#sp_blitzfirst-real-time-performance-advice) + - [sp_BlitzIndex: Tune Your Indexes](#sp_blitzindex-tune-your-indexes) + - [Advanced sp_BlitzIndex Parameters](#advanced-sp_blitzindex-parameters) + - Performance Tuning: + - [sp_BlitzLock: Deadlock Analysis](#sp_blitzlock-deadlock-analysis) + - [sp_BlitzWho: What Queries are Running Now](#sp_blitzwho-what-queries-are-running-now) + - [sp_BlitzAnalysis: Query sp_BlitzFirst output tables](#sp_blitzanalysis-query-sp_BlitzFirst-output-tables) - Backups and Restores: - [sp_BlitzBackups: How Much Data Could You Lose](#sp_blitzbackups-how-much-data-could-you-lose) - - [sp_AllNightLog: Back Up Faster to Lose Less Data](#sp_allnightlog-back-up-faster-to-lose-less-data) - [sp_DatabaseRestore: Easier Multi-File Restores](#sp_databaserestore-easier-multi-file-restores) - [Parameters Common to Many of the Stored Procedures](#parameters-common-to-many-of-the-stored-procedures) - - [Power BI Dashboard for DBAs](#power-bi-dashboard-for-dbas) - [License MIT](#license) You're a DBA, sysadmin, or developer who manages Microsoft SQL Servers. It's your fault if they're down or slow. These tools help you understand what's going on in your server. @@ -38,17 +40,30 @@ To install, [download the latest release ZIP](https://github.com/BrentOzarULTD/S The First Responder Kit runs on: -* SQL Server 2008, 2008R2, 2012, 2014, 2016, 2017 on Windows - yes, fully supported -* SQL Server 2017 on Linux - yes, fully supported except sp_AllNightLog and sp_DatabaseRestore, which require xp_cmdshell, which Microsoft doesn't provide on Linux -* SQL Server 2000, 2005 - not supported by Microsoft anymore, so we don't either -* Amazon RDS SQL Server - fully supported -* Azure SQL DB - It's a dice roll. Microsoft changes DMV contents in here without warning, so no guarantees. +* SQL Server on Windows - all versions that Microsoft supports. For end of support dates, check out the "Support Ends" column at https://sqlserverupdates.com. +* SQL Server on Linux - yes, fully supported except sp_DatabaseRestore, which require xp_cmdshell, which Microsoft doesn't provide on Linux. +* Amazon RDS SQL Server - fully supported. +* Azure SQL DB - not supported. Some of the procedures work, but some don't, and Microsoft has a tendency to change DMVs in Azure without warning, so we don't put any effort into supporting it. If it works, great! If not, any changes to make it work would be on you. [See the contributing.md file](CONTRIBUTING.md) for how to do that. + +If you're stuck with versions of SQL Server that Microsoft no longer supports, like SQL Server 2008, check the Deprecated folder for older versions of the scripts which may work, depending on your versions and compatibility levels. + +## How to Install the Scripts + +For SQL Server, to install all of the scripts at once, open Install-All-Scripts.sql in SSMS or Azure Data Studio, switch to the database where you want to install the procs, and run it. It will install the stored procedures if they don't already exist, or update them to the current version if they do exist. + +For Azure SQL DB, use Install-Azure.sql, which will only install the procs that are compatible with Azure SQL DB. + +If you hit an error when running Install-All-Scripts, it's likely because you're using an older version of SQL Server that Microsoft no longer supports. In that case, check out the Deprecated folder. That's where we keep old versions of the scripts around as a last-ditch effort - but really, if Microsoft won't support their own old stuff, you shouldn't try to do it either. + +We recommend installing these stored procedures in the master database, but if you want to use another one, that's totally fine - they're all supported in any database - but just be aware that you can run into problems if you have these procs in multiple databases. You may not keep them all up to date, and you may hit an issue when you're running an older version. + +There are a couple of Install-Core scripts included for legacy purposes, for folks with installers they've built. You can ignore those. ## How to Get Support Everyone here is expected to abide by the [Contributor Covenant Code of Conduct](CONTRIBUTING.md#the-contributor-covenant-code-of-conduct). -When you have questions about how the tools work, talk with the community in the [#FirstResponderKit Slack channel](https://sqlcommunity.slack.com/messages/firstresponderkit/). If you need a free invite, hit [SQLslack.com](https://SQLslack.com/). Be patient - it's staffed with volunteers who have day jobs, heh. +When you have questions about how the tools work, talk with the community in the [#FirstResponderKit Slack channel](https://sqlcommunity.slack.com/messages/firstresponderkit/). If you need a free invite, hit [SQLslack.com](http://SQLslack.com/). Be patient - it's staffed with volunteers who have day jobs, heh. When you find a bug or want something changed, [read the contributing.md file](CONTRIBUTING.md). @@ -58,9 +73,7 @@ When you have a question about what the scripts found, first make sure you read ## sp_Blitz: Overall Health Check -Run sp_Blitz daily or weekly for an overall health check. Just run it from SQL Server Management Studio, and you'll get a prioritized list of issues on your server right now: - -![sp_Blitz](http://u.brentozar.com/github-images/sp_Blitz.png) +Run sp_Blitz daily or weekly for an overall health check. Just run it from SQL Server Management Studio, and you'll get a prioritized list of issues on your server right now. Output columns include: @@ -77,18 +90,24 @@ Commonly used parameters: * @CheckServerInfo = 1 - includes additional rows at priority 250 with server configuration details like service accounts. * @IgnorePrioritiesAbove = 50 - if you want a daily bulletin of the most important warnings, set @IgnorePrioritiesAbove = 50 to only get the urgent stuff. +Advanced tips: + +* [How to install, run, and centralize the data from sp_Blitz using PowerShell](https://garrybargsley.com/2020/07/14/sp_blitz-for-all-servers/) + [*Back to top*](#header1) ### Advanced sp_Blitz Parameters In addition to the [parameters common to many of the stored procedures](#parameters-common-to-many-of-the-stored-procedures), here are the ones specific to sp_Blitz: +* @Debug default 0. When 1, we print out messages of what we're doing. When 2, we print the dynamic queries as well + [*Back to top*](#header1) #### Writing sp_Blitz Output to a Table -```SQL -sp_Blitz @OutputDatabaseName = 'DBAtools', @OutputSchemaName = 'dbo', @OutputTableName = 'BlitzResults'; +```tsql +EXEC sp_Blitz @OutputDatabaseName = 'DBAtools', @OutputSchemaName = 'dbo', @OutputTableName = 'BlitzResults'; ``` Checks for the existence of a table DBAtools.dbo.BlitzResults, creates it if necessary, then adds the output of sp_Blitz into this table. This table is designed to support multiple outputs from multiple servers, so you can track your server's configuration history over time. @@ -97,16 +116,16 @@ Checks for the existence of a table DBAtools.dbo.BlitzResults, creates it if nec #### Skipping Checks or Databases -```SQL +```tsql CREATE TABLE dbo.BlitzChecksToSkip ( -ServerName NVARCHAR(128), -DatabaseName NVARCHAR(128), -CheckID INT + ServerName NVARCHAR(128), + DatabaseName NVARCHAR(128), + CheckID INT ); GO INSERT INTO dbo.BlitzChecksToSkip (ServerName, DatabaseName, CheckID) VALUES (NULL, 'SalesDB', 50) -sp_Blitz @SkipChecksDatabase = 'DBAtools', @SkipChecksSchema = 'dbo', @SkipChecksTable = 'BlitzChecksToSkip' +sp_Blitz @SkipChecksDatabase = 'DBAtools', @SkipChecksSchema = 'dbo', @SkipChecksTable = 'BlitzChecksToSkip'; ``` Checks for the existence of a table named Fred - just kidding, named DBAtools.dbo.BlitzChecksToSkip. The table needs at least the columns shown above (ServerName, DatabaseName, and CheckID). For each row: @@ -149,14 +168,19 @@ The @SortOrder parameter lets you pick which top 10 queries you want to examine: * memory grant - if you're troubleshooting a RESOURCE_SEMAPHORE issue and want to find queries getting a lot of memory * writes - if you wanna find those pesky ETL processes * You can also use average or avg for a lot of the sorts, like @SortOrder = 'avg reads' -* all - sorts by all the different sort order options, and returns a single result set of hot messes. (Note that @SortOrder = 'all' is incompatible with the @OutputTableName parameter because it produces a different result set shape.) +* all - sorts by all the different sort order options, and returns a single result set of hot messes. This is a little tricky because: +* We find the @Top N queries by CPU, then by reads, then writes, duration, executions, memory grant, spills, etc. If you want to set @Top > 10, you also have to set @BringThePain = 1 to make sure you understand that it can be pretty slow. +* As we work through each pattern, we exclude the results from the prior patterns. So for example, we get the top 10 by CPU, and then when we go to get the top 10 by reads, we exclude queries that were already found in the top 10 by CPU. As a result, the top 10 by reads may not really be the top 10 by reads - because some of those might have been in the top 10 by CPU. +* To make things even a little more confusing, in the Pattern column of the output, we only specify the first pattern that matched, not all of the patterns that matched. It would be cool if at some point in the future, we turned this into a comma-delimited list of patterns that a query matched, and then we'd be able to get down to a tighter list of top queries. For now, though, this is kinda unscientific. +* query hash - filters for only queries that have multiple cached plans (even though they may all still be the same plan, just different copies stored.) If you use @SortOrder = 'query hash', you can specify a second sort order with a comma, like 'query hash, reads' in order to find only queries with multiple plans, sorted by the ones doing the most reads. The default second sort is CPU. Other common parameters include: * @Top = 10 - by default, you get 10 plans, but you can ask for more. Just know that the more you get, the slower it goes. * @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. +* @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) @@ -165,43 +189,18 @@ Other common parameters include: In addition to the [parameters common to many of the stored procedures](#parameters-common-to-many-of-the-stored-procedures), here are the ones specific to sp_BlitzCache: -* OnlyQueryHashes - if you want to examine specific query plans, you can pass in a comma-separated list of them in a string. -* IgnoreQueryHashes - if you know some queries suck and you don't want to see them, you can pass in a comma-separated list of them. -* OnlySqlHandles, @IgnoreSqlHandles - just like the above two params +* @OnlyQueryHashes - if you want to examine specific query plans, you can pass in a comma-separated list of them in a string. +* @IgnoreQueryHashes - if you know some queries suck and you don't want to see them, you can pass in a comma-separated list of them. +* @OnlySqlHandles, @IgnoreSqlHandles - just like the above two params * @DatabaseName - if you only want to analyze plans in a single database. However, keep in mind that this is only the database context. A single query that runs in Database1 can join across objects in Database2 and Database3, but we can only know that it ran in Database1. +* @SlowlySearchPlansFor - lets you search for strings, but will not find all results due to a [bug in the way SQL Server removes spaces from XML.](https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2202) If your search string includes spaces, SQL Server may remove those before the search runs, unfortunately. -[*Back to top*](#header1) - - -## sp_BlitzIndex: Tune Your Indexes - -SQL Server tracks your indexes: how big they are, how often they change, whether they're used to make queries go faster, and which indexes you should consider adding. The results columns are fairly self-explanatory. - -By default, sp_BlitzIndex analyzes the indexes of the database you're in (your current context.) - -Common parameters include: - -* @DatabaseName - if you want to analyze a specific database -* @SchemaName, @TableName - if you pass in these, sp_BlitzIndex does a deeper-dive analysis of just one table. You get several result sets back describing more information about the table's current indexes, foreign key relationships, missing indexes, and fields in the table. -* @GetAllDatabases = 1 - slower, but lets you analyze all the databases at once, up to 50. If you want more than 50 databases, you also have to pass in @BringThePain = 1. -* @ThresholdMB = 250 - by default, we only analyze objects over 250MB because you're busy. -* @Mode = 0 (default) - get different data with 0=Diagnose, 1=Summarize, 2=Index Usage Detail, 3=Missing Index Detail, 4=Diagnose Details. - - -[*Back to top*](#header1) - -### Advanced sp_BlitzIndex Parameters - -In addition to the [parameters common to many of the stored procedures](#parameters-common-to-many-of-the-stored-procedures), here are the ones specific to sp_BlitzIndex: - -* @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. -* @Filter = 0 (default) - 1=No low-usage warnings for objects with 0 reads. 2=Only warn for objects >= 500MB +### sp_BlitzCache Known Issues +* We skip databases in an Availability Group that require read-only intent. If you wanted to contribute code to enable read-only intent databases to work, look for this phrase in the code: "Checking for Read intent databases to exclude". [*Back to top*](#header1) - ## sp_BlitzFirst: Real-Time Performance Advice When performance emergencies strike, this should be the first stored proc in the kit you run. @@ -218,32 +217,31 @@ Common sp_BlitzFirst parameters include: * @Seconds = 5 by default. You can specify longer samples if you want to track stats during a load test or demo, for example. * @ShowSleepingSPIDs = 0 by default. When set to 1, shows long-running sleeping queries that might be blocking others. -* @ExpertMode = 0 by default. When set to 1, it calls sp_BlitzWho when it starts (to show you what queries are running right now), plus outputs additional result sets for wait stats, Perfmon counters, and file stats during the sample, then finishes with one final execution of sp_BlitzWho to show you what was running at the end of the sample. +* @ExpertMode = 0 by default. When set to 1, it calls sp_BlitzWho when it starts (to show you what queries are running right now), plus outputs additional result sets for wait stats, Perfmon counters, and file stats during the sample, then finishes with one final execution of sp_BlitzWho to show you what was running at the end of the sample. When set to 2, it does the same as 1, but skips the calls to sp_BlitzWho. ### Logging sp_BlitzFirst to Tables -You can log sp_BlitzFirst performance data to tables and then analyze the results with the Power BI dashboard. To do it, schedule an Agent job to run sp_BlitzFirst every 15 minutes with these parameters populated: +You can log sp_BlitzFirst performance data to tables by scheduling an Agent job to run sp_BlitzFirst every 15 minutes with these parameters populated: * @OutputDatabaseName = typically 'DBAtools' * @OutputSchemaName = 'dbo' * @OutputTableName = 'BlitzFirst' - the quick diagnosis result set goes here -* @OutputTableName_FileStats = 'BlitzFirst_FileStats' -* @OutputTableName_PerfmonStats = 'BlitzFirst_PerfmonStats' -* @OutputTableName_WaitStats = 'BlitzFirst_WaitStats' -* @OutputTableName_BlitzCache = 'BlitzCache' +* @OutputTableNameFileStats = 'BlitzFirst_FileStats' +* @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' +* @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' +* @OutputTableNameBlitzCache = 'BlitzCache' +* @OutputTableNameBlitzWho = 'BlitzWho' All of the above OutputTableName parameters are optional: if you don't want to collect all of the stats, you don't have to. Keep in mind that the sp_BlitzCache results will get large, fast, because each execution plan is megabytes in size. -Then fire up the [First Responder Kit Power BI dashboard.](https://www.brentozar.com/first-aid/first-responder-kit-power-bi-dashboard/) - ### Logging Performance Tuning Activities -On the Power BI Dashboard, you can show lines for your own activities like tuning queries, adding indexes, or changing configuration settings. To do it, run sp_BlitzFirst with these parameters: +You can also log your own activities like tuning queries, adding indexes, or changing configuration settings. To do it, run sp_BlitzFirst with these parameters: * @OutputDatabaseName = typically 'DBAtools' * @OutputSchemaName = 'dbo' * @OutputTableName = 'BlitzFirst' - the quick diagnosis result set goes here -* @LogMessage = 'Whatever you wanna show in the Power BI dashboard' +* @LogMessage = 'Whatever you wanna show in your monitoring tool' Optionally, you can also pass in: @@ -255,56 +253,164 @@ Optionally, you can also pass in: [*Back to top*](#header1) +## sp_BlitzIndex: Tune Your Indexes -## sp_BlitzWho: What Queries are Running Now +SQL Server tracks your indexes: how big they are, how often they change, whether they're used to make queries go faster, and which indexes you should consider adding. The results columns are fairly self-explanatory. -This is like sp_who, except it goes into way, way, way more details. +By default, sp_BlitzIndex analyzes the indexes of the database you're in (your current context.) + +Common parameters include: + +* @DatabaseName - if you want to analyze a specific database +* @SchemaName, @TableName - if you pass in these, sp_BlitzIndex does a deeper-dive analysis of just one table. You get several result sets back describing more information about the table's current indexes, foreign key relationships, missing indexes, and fields in the table. +* @GetAllDatabases = 1 - slower, but lets you analyze all the databases at once, up to 50. If you want more than 50 databases, you also have to pass in @BringThePain = 1. +* @ThresholdMB = 250 - by default, we only analyze objects over 250MB because you're busy. +* @Mode = 0 (default) - returns high-priority (1-100) advice on the most urgent index issues. + * @Mode = 4: Diagnose Details - like @Mode 0, but returns even more advice (priorities 1-255) with things you may not be able to fix right away, and things we just want to warn you about. + * @Mode = 1: Summarize - total numbers of indexes, space used, etc per database. + * @Mode = 2: Index Usage Details - an inventory of your existing indexes and their usage statistics. Great for copy/pasting into Excel to do slice & dice analysis. This is the only mode that works with the @Output parameters: you can export this data to table on a monthly basis if you need to go back and look to see which indexes were used over time. + * @Mode = 3: Missing Indexes - an inventory of indexes SQL Server is suggesting. Also great for copy/pasting into Excel for later analysis. + +sp_BlitzIndex focuses on mainstream index types. Other index types have varying amounts of support: + +* Fully supported: rowstore indexes, columnstore indexes, temporal tables. +* Columnstore indexes: fully supported. Key columns are shown as includes rather than keys since they're not in a specific order. +* Graph tables: unsupported. These objects show up in the results, but we don't do anything special with 'em, like call out that they're graph tables. +* Spatial indexes: unsupported. We call out that they're spatial, but we don't do any special handling for them. +* XML indexes: unsupported. These objects show up in the results, but we don't include the index's columns or sizes. -It's designed for query tuners, so it includes things like memory grants, degrees of parallelism, and execution plans. [*Back to top*](#header1) -## sp_BlitzQueryStore: Query Store Sale - -Analyzes data in Query Store schema (2016+ only) in many similar ways to what sp_BlitzCache does for the plan cache. - -* @Help: Right now this just prints the license if set to 1. I'm going to add better documentation here as the script matures. -* @DatabaseName: This one is required. Query Store is per database, so you have to point it at one to examine. -* @Top: How many plans from each "worst" you want to get. We look at your maxes for CPU, reads, duration, writes, memory, rows, executions, and additionally tempdb and log bytes for 2017. So it's the number of plans from each of those to gather. -* @StartDate: Fairly obvious, when you want to start looking at queries from. If NULL, we'll only go back seven days. -* @EndDate: When you want to stop looking at queries from. If you leave it NULL, we'll look ahead seven days. -* @MinimumExecutionCount: The minimum number of times a query has to have been executed (not just compiled) to be analyzed. -* @DurationFilter: The minimum number of seconds a query has to have been executed for to be analyzed. -* @StoredProcName: If you want to look at a single stored procedure. -* @Failed: If you want to look at failed queries, for some reason. I dunno, MS made such a big deal out of being able to look at these, I figured I'd add it. -* @PlanIdFilter: If you want to filter by a particular plan id. Remember that a query may have many different plans. -* @QueryIdFilter: If you want to filter by a particular query id. If you want to look at one specific plan for a query. -* @ExportToExcel: Leaves XML out of the input and tidies up query text so you can easily paste it into Excel. -* @HideSummary: Pulls the rolled up warnings and information out of the results. -* @SkipXML: Skips XML analysis. -* @Debug: Prints dynamic SQL and selects data from all temp tables if set to 1. +### Advanced sp_BlitzIndex Parameters + +In addition to the [parameters common to many of the stored procedures](#parameters-common-to-many-of-the-stored-procedures), here are the ones specific to sp_BlitzIndex: + +* @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. + [*Back to top*](#header1) + + ## sp_BlitzLock: Deadlock Analysis Checks either the System Health session or a specific Extended Event session that captures deadlocks and parses out all the XML for you. -Variables you can use: -* @Top: Use if you want to limit the number of deadlocks to return. This is ordered by event date ascending. +Parameters you can use: * @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. * @ObjectName: If you want to filter to a specific table. The object name has to be fully qualified 'Database.Schema.Table' -* @StoredProcName: If you want to search for a single stored proc. +* @StoredProcName: If you want to search for a single stored procedure. Don't specify a schema or database name - just a stored procedure name alone is all you need, and if it exists in any schema (or multiple schemas), we'll find it. * @AppName: If you want to filter to a specific application. * @HostName: If you want to filter to a specific host. * @LoginName: If you want to filter to a specific login. * @EventSessionPath: If you want to point this at an XE session rather than the system health session. +Known issues: + +* 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) + + +[*Back to top*](#header1) + + +## sp_BlitzWho: What Queries are Running Now + +This is like sp_who, except it goes into way, way, way more details. + +It's designed for query tuners, so it includes things like memory grants, degrees of parallelism, and execution plans. [*Back to top*](#header1) + +## sp_BlitzAnalysis: Query sp_BlitzFirst output tables + +Retrieves data from the output tables where you are storing your sp_BlitzFirst output. + +Parameters include: + +* @StartDate: When you want to start seeing data from , NULL will set @StartDate to 1 hour ago. +* @EndDate: When you want to see data up to, NULL will get an hour of data since @StartDate. +* @Databasename: Filter results by database name where possible, Default: NULL which shows all. +* @Servername: Filter results by server name, Default: @@SERVERNAME. +* @OutputDatabaseName: Specify the database name where where we can find your logged sp_BlitzFirst Output table data +* @OutputSchemaName: Schema which the sp_BlitzFirst Output tables belong to +* @OutputTableNameBlitzFirst: Table name where you are storing sp_BlitzFirst @OutputTableNameBlitzFirst output, we default to BlitzFirst - you can Set to NULL to skip lookups against this table +* @OutputTableNameFileStats: Table name where you are storing sp_BlitzFirst @OutputTableNameFileStats output, we default to BlitzFirst_FileStats - you can Set to NULL to skip lookups against this table. +* @OutputTableNamePerfmonStats: Table name where you are storing sp_BlitzFirst @OutputTableNamePerfmonStats output, we default to BlitzFirst_PerfmonStats - you can Set to NULL to skip lookups against this table. +* @OutputTableNameWaitStats: Table name where you are storing sp_BlitzFirst @OutputTableNameWaitStats output, we default to BlitzFirst_WaitStats - you can Set to NULL to skip lookups against this table. +* @OutputTableNameBlitzCache: Table name where you are storing sp_BlitzFirst @OutputTableNameBlitzCache output, we default to BlitzCache - you can Set to NULL to skip lookups against this table. +* @OutputTableNameBlitzWho: Table name where you are storing sp_BlitzFirst @OutputTableNameBlitzWho output, we default to BlitzWho - you can Set to NULL to skip lookups against this table. +* @MaxBlitzFirstPriority: Max priority to include in the results from your BlitzFirst table, Default: 249. +* @BlitzCacheSortorder: Controls the results returned from your BlitzCache table, you will get a TOP 5 per sort order per CheckDate, Default: 'cpu' Accepted values 'all' 'cpu' 'reads' 'writes' 'duration' 'executions' 'memory grant' 'spills'. +* @WaitStatsTop: Controls the Top X waits per CheckDate from your wait stats table, Default: 10. +* @ReadLatencyThreshold: Sets the threshold in ms to compare against io_stall_read_average_ms in your filestats table, Default: 100. +* @WriteLatencyThreshold: Sets the threshold in ms to compare against io_stall_write_average_ms in your filestats table, Default: 100. +* @BringThePain: If you are getting more than 4 hours of data from your BlitzCache table with @BlitzCacheSortorder set to 'all' you will need to set BringThePain to 1. +* @Maxdop: Control the degree of parallelism that the queries within this proc can use if they want to, Default = 1. +* @Debug: Show sp_BlitzAnalysis SQL commands in the messages tab as they execute. + +Example calls: + +Get information for the last hour from all sp_BlitzFirst output tables + +```tsql +EXEC sp_BlitzAnalysis + @StartDate = NULL, + @EndDate = NULL, + @OutputDatabaseName = 'DBAtools', + @OutputSchemaName = 'dbo', + @OutputTableNameFileStats = N'BlitzFirst_FileStats', + @OutputTableNamePerfmonStats = N'BlitzFirst_PerfmonStats', + @OutputTableNameWaitStats = N'BlitzFirst_WaitStats', + @OutputTableNameBlitzCache = N'BlitzCache', + @OutputTableNameBlitzWho = N'BlitzWho'; +``` + +Exclude specific tables e.g lets exclude PerfmonStats by setting to NULL, no lookup will occur against the table and a skipped message will appear in the resultset + +```tsql +EXEC sp_BlitzAnalysis + @StartDate = NULL, + @EndDate = NULL, + @OutputDatabaseName = 'DBAtools', + @OutputSchemaName = 'Blitz', + @OutputTableNameFileStats = N'BlitzFirst_FileStats', + @OutputTableNamePerfmonStats = NULL, + @OutputTableNameWaitStats = N'BlitzFirst_WaitStats', + @OutputTableNameBlitzCache = N'BlitzCache', + @OutputTableNameBlitzWho = N'BlitzWho'; +``` + +Known issues: +We are likely to be hitting some big tables here and some of these queries will require scans of the clustered indexes as there are no nonclustered indexes to cover the queries by default, keep this in mind if you are planning on running this in a production environment! + +I have noticed that the Perfmon query can ask for a big memory grant so be mindful when including this table with large volumes of data: + +```tsql +SELECT + [ServerName] + , [CheckDate] + , [counter_name] + , [object_name] + , [instance_name] + , [cntr_value] +FROM [dbo].[BlitzFirst_PerfmonStats_Actuals] +WHERE CheckDate BETWEEN @FromDate AND @ToDate +ORDER BY + [CheckDate] ASC + , [counter_name] ASC; +``` + +[*Back to top*](#header1) + + ## sp_BlitzBackups: How Much Data Could You Lose Checks your backups and reports estimated RPO and RTO based on historical data in msdb, or a centralized location for [msdb].dbo.backupset. @@ -314,10 +420,7 @@ Parameters include: * @HoursBack -- How many hours into backup history you want to go. Should be a negative number (we're going back in time, after all). But if you enter a positive number, we'll make it negative for you. You're welcome. * @MSDBName -- if you need to prefix dbo.backupset with an alternate database name. * @AGName -- If you have more than 1 AG on the server, and you don't know the listener name, specify the name of the AG you want to use the listener for, to push backup data. This may get used during analysis in a future release for filtering. -* @RestoreSpeedFullMBps --[FIXFIX] Brent can word this better than I can -* @RestoreSpeedDiffMBps -- Nothing yet -* @RestoreSpeedLogMBps -- Nothing yet - +* @RestoreSpeedFullMBps, @RestoreSpeedDiffMBps, @RestoreSpeedLogMBps -- if you know your restore speeds, you can input them here to better calculate your worst-case RPO times. Otherwise, we assume that your restore speed will be the same as your backup speed. That isn't likely true - your restore speed will likely be worse - but these numbers already scare the pants off people. * @PushBackupHistoryToListener -- Turn this to 1 to skip analysis and use sp_BlitzBackups to push backup data from msdb to a centralized location (more the mechanics of this to follow) * @WriteBackupsToListenerName -- This is the name of the AG listener, and **MUST** have a linked server configured pointing to it. Yes, that means you need to create a linked server that points to the AG Listener, with the appropriate permissions to write data. * @WriteBackupsToDatabaseName -- This can't be 'msdb' if you're going to use the backup data pushing mechanism. We can't write to your actual msdb tables. @@ -325,11 +428,11 @@ Parameters include: An example run of sp_BlitzBackups to push data looks like this: -``` -EXEC sp_BlitzBackups @PushBackupHistoryToListener = 1, -- Turn it on! - @WriteBackupsToListenerName = 'AG_LISTENER_NAME', -- Name of AG Listener and Linked Server - @WriteBackupsToDatabaseName = 'FAKE_MSDB_NAME', -- Fake MSDB name you want to push to. Remember, can't be real MSDB. - @WriteBackupsLastHours = -24 -- Hours back in time you want to go +```tsql +EXEC sp_BlitzBackups @PushBackupHistoryToListener = 1, -- Turn it on! + @WriteBackupsToListenerName = 'AG_LISTENER_NAME', -- Name of AG Listener and Linked Server + @WriteBackupsToDatabaseName = 'FAKE_MSDB_NAME', -- Fake MSDB name you want to push to. Remember, can't be real MSDB. + @WriteBackupsLastHours = -24 -- Hours back in time you want to go ``` In an effort to not clog your servers up, we've taken some care in batching things as we move data. Inspired by [Michael J. Swart's Take Care When Scripting Batches](http://michaeljswart.com/2014/09/take-care-when-scripting-batches/), we only move data in 10 minute intervals. @@ -339,28 +442,6 @@ The reason behind that is, if you have 500 databases, and you're taking log back [*Back to top*](#header1) -## sp_AllNightLog: Back Up Faster to Lose Less Data - -You manage a SQL Server instance with hundreds or thousands of mission-critical databases. You want to back them all up as quickly as possible, and one maintenance plan job isn't going to cut it. - -Let's scale out our backup jobs by: - -* Creating a table with a list of databases and their desired Recovery Point Objective (RPO, aka data loss) - done with sp_AllNightLog_Setup -* Set up several Agent jobs to back up databases as necessary - also done with sp_AllNightLog_Setup -* Inside each of those Agent jobs, they call sp_AllNightLog @Backup = 1, which loops through the table to find databases that need to be backed up, then call [Ola Hallengren's DatabaseBackup stored procedure](https://ola.hallengren.com/) -* Keeping that database list up to date as new databases are added - done by a job calling sp_AllNightLog @PollForNewDatabases = 1 - -For more information about how this works, see [sp_AllNightLog documentation.](https://www.BrentOzar.com/sp_AllNightLog) - -Known issues: - -* The msdbCentral database name is hard-coded. -* sp_AllNightLog depends on Ola Hallengren's DatabaseBackup, which must be installed separately. (We're not checking for it right now.) - - -[*Back to top*](#header1) - - ## sp_DatabaseRestore: Easier Multi-File Restores If you use [Ola Hallengren's backup scripts](http://ola.hallengren.com), DatabaseRestore.sql helps you rapidly restore a database to the most recent point in time. @@ -369,19 +450,22 @@ Parameters include: * @Database - the database's name, like LogShipMe * @RestoreDatabaseName -* @BackupPathFull - typically a UNC path like '\\FILESERVER\BACKUPS\SQL2016PROD1A\LogShipMe\FULL\' that points to where the full backups are stored. Note that if the path doesn't exist, we don't create it, and the query might take 30+ seconds if you specify an invalid server name. +* @BackupPathFull - typically a UNC path like '\\\\FILESERVER\BACKUPS\SQL2016PROD1A\LogShipMe\FULL\' that points to where the full backups are stored. Note that if the path doesn't exist, we don't create it, and the query might take 30+ seconds if you specify an invalid server name. * @BackupPathDiff, @BackupPathLog - as with the Full, this should be set to the exact path where the differentials and logs are stored. We don't append anything to these parameters. * @MoveFiles, @MoveDataDrive, @MoveLogDrive - if you want to restore to somewhere other than your default database locations. +* @FileNamePrefix - Prefix to add to the names of all restored files. Useful when you need to restore different backups of the same database into the same directory. * @RunCheckDB - default 0. When set to 1, we run Ola Hallengren's DatabaseIntegrityCheck stored procedure on this database, and log the results to table. We use that stored proc's default parameters, nothing fancy. * @TestRestore - default 0. When set to 1, we delete the database after the restore completes. Used for just testing your restores. Especially useful in combination with @RunCheckDB = 1 because we'll delete the database after running checkdb, but know that we delete the database even if it fails checkdb tests. * @RestoreDiff - default 0. When set to 1, we restore the ncessary full, differential, and log backups (instead of just full and log) to get to the most recent point in time. * @ContinueLogs - default 0. When set to 1, we don't restore a full or differential backup - we only restore the transaction log backups. Good for continuous log restores with tools like sp_AllNightLog. * @RunRecovery - default 0. When set to 1, we run RESTORE WITH RECOVERY, putting the database into writable mode, and no additional log backups can be restored. +* @ExistingDBAction - if the database already exists when we try to restore it, 1 sets the database to single user mode, 2 kills the connections, and 3 kills the connections and then drops the database. * @Debug - default 0. When 1, we print out messages of what we're doing in the messages tab of SSMS. -* @StopAt NVARCHAR(14) - pass in a date time to stop your restores at a time like '20170508201501'. - +* @StopAt NVARCHAR(14) - pass in a date time to stop your restores at a time like '20170508201501'. This doesn't use the StopAt parameter for the restore command - it simply stops restoring logs that would have this date/time's contents in it. (For example, if you're taking backups every 15 minutes on the hour, and you pass in 9:05 AM as part of the restore time, the restores would stop at your last log backup that doesn't include 9:05AM's data - but it won't restore right up to 9:05 AM.) +* @SkipBackupsAlreadyInMsdb - default 0. When set to 1, we check MSDB for the most recently restored backup from this log path, and skip all backup files prior to that. Useful if you're pulling backups from across a slow network and you don't want to wait to check the restore header of each backup. +* @EnableBroker - default 0. When set to 1, we run RESTORE WITH ENABLE_BROKER, enabling the service broker. Unless specified, the service broker is disabled on restore even if it was enabled when the backup was taken. -For information about how this works, see [Tara Kizer's white paper on Log Shipping 2.0 with Google Compute Engine.](https://BrentOzar.com/go/gce) +For information about how this works, see [Tara Kizer's white paper on Log Shipping 2.0 with Google Compute Engine.](https://www.brentozar.com/archive/2017/03/new-white-paper-build-sql-server-disaster-recovery-plan-google-compute-engine/) [*Back to top*](#header1) @@ -391,16 +475,20 @@ For information about how this works, see [Tara Kizer's white paper on Log Shipp * @Help = 1 - returns a result set or prints messages explaining the stored procedure's input and output. Make sure to check the Messages tab in SSMS to read it. * @ExpertMode = 1 - turns on more details useful for digging deeper into results. -* @OutputDatabaseName, @OutputSchemaName, @OutputTableName - pass all three of these in, and the stored proc's output will be written to a table. We'll create the table if it doesn't already exist. -* @OutputServerName - not functional yet. To track (or help!) implementation status: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/293 - -[*Back to top*](#header1) - -## Power BI Dashboard for DBAs - -[Documentation for this part of the project is currently at BrentOzar.com.](https://www.brentozar.com/first-aid/first-responder-kit-power-bi-dashboard/) - -To contribute changes, read the [contributing guide](CONTRIBUTING.md) - there's a special section for the Power BI Dashboard since it has to be changed differently than scripts. +* @OutputDatabaseName, @OutputSchemaName, @OutputTableName - pass all three of these in, and the stored proc's output will be written to a table. We'll create the table if it doesn't already exist. @OutputServerName will push the data to a linked server as long as you configure the linked server first and enable RPC OUT calls. + +To check versions of any of the stored procedures, use their output parameters for Version and VersionDate like this: + +```tsql +DECLARE @VersionOutput VARCHAR(30), @VersionDateOutput DATETIME; +EXEC sp_Blitz + @Version = @VersionOutput OUTPUT, + @VersionDate = @VersionDateOutput OUTPUT, + @VersionCheckMode = 1; +SELECT + @VersionOutput AS Version, + @VersionDateOutput AS VersionDate; +``` [*Back to top*](#header1) @@ -416,8 +504,10 @@ To contribute changes, read the [contributing guide](CONTRIBUTING.md) - there's [stars badge]:https://img.shields.io/github/stars/BrentOzarULTD/SQL-Server-First-Responder-Kit.svg [forks badge]:https://img.shields.io/github/forks/BrentOzarULTD/SQL-Server-First-Responder-Kit.svg [issues badge]:https://img.shields.io/github/issues/BrentOzarULTD/SQL-Server-First-Responder-Kit.svg +[contributors_badge]:https://img.shields.io/github/contributors/BrentOzarULTD/SQL-Server-First-Responder-Kit.svg [licence]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/master/LICENSE.md [stars]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/stargazers [forks]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/network [issues]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues +[contributors]:https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/graphs/contributors diff --git a/SqlServerVersions.sql b/SqlServerVersions.sql new file mode 100644 index 000000000..3bb177a6a --- /dev/null +++ b/SqlServerVersions.sql @@ -0,0 +1,497 @@ + +IF (OBJECT_ID('dbo.SqlServerVersions') IS NULL) +BEGIN + + CREATE TABLE dbo.SqlServerVersions + ( + MajorVersionNumber tinyint not null, + MinorVersionNumber smallint not null, + Branch varchar(34) not null, + [Url] varchar(99) not null, + ReleaseDate date not null, + MainstreamSupportEndDate date not null, + ExtendedSupportEndDate date not null, + MajorVersionName varchar(19) not null, + MinorVersionName varchar(67) not null, + + CONSTRAINT PK_SqlServerVersions PRIMARY KEY CLUSTERED + ( + MajorVersionNumber ASC, + MinorVersionNumber ASC, + ReleaseDate ASC + ) + ); + + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The major version number.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MajorVersionNumber' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The minor version number.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MinorVersionNumber' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The update level of the build. CU indicates a cumulative update. SP indicates a service pack. RTM indicates Release To Manufacturer. GDR indicates a General Distribution Release. QFE indicates Quick Fix Engineering (aka hotfix).' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'Branch' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'A link to the KB article for a version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'Url' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date the version was publicly released.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'ReleaseDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date main stream Microsoft support ends for the version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MainstreamSupportEndDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The date extended Microsoft support ends for the version.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'ExtendedSupportEndDate' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The major version name.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MajorVersionName' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'The minor version name.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions', @level2type=N'COLUMN',@level2name=N'MinorVersionName' + EXEC sys.sp_addextendedproperty @name=N'Description', @value=N'A reference for SQL Server major and minor versions.' , @level0type=N'SCHEMA',@level0name=N'dbo', @level1type=N'TABLE',@level1name=N'SqlServerVersions' + +END; +GO + +DELETE FROM dbo.SqlServerVersions; + +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'), + /*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'), + (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'), + (16, 4140, 'CU14 GDR', 'https://support.microsoft.com/en-us/help/5042578', '2024-09-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 14 GDR'), + (16, 4135, 'CU14', 'https://support.microsoft.com/en-us/help/5038325', '2024-07-23', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 14'), + (16, 4125, 'CU13', 'https://support.microsoft.com/en-us/help/5036432', '2024-05-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 13'), + (16, 4115, 'CU12', 'https://support.microsoft.com/en-us/help/5033663', '2024-03-14', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 12'), + (16, 4105, 'CU11', 'https://support.microsoft.com/en-us/help/5032679', '2024-01-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 11'), + (16, 4100, 'CU10 GDR', 'https://support.microsoft.com/en-us/help/5033592', '2024-01-09', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 10 GDR'), + (16, 4095, 'CU10', 'https://support.microsoft.com/en-us/help/5031778', '2023-11-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 10'), + (16, 4085, 'CU9', 'https://support.microsoft.com/en-us/help/5030731', '2023-10-12', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 9'), + (16, 4075, 'CU8', 'https://support.microsoft.com/en-us/help/5029666', '2023-09-14', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 8'), + (16, 4065, 'CU7', 'https://support.microsoft.com/en-us/help/5028743', '2023-08-10', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 7'), + (16, 4055, 'CU6', 'https://support.microsoft.com/en-us/help/5027505', '2023-07-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 6'), + (16, 4045, 'CU5', 'https://support.microsoft.com/en-us/help/5026806', '2023-06-15', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 5'), + (16, 4035, 'CU4', 'https://support.microsoft.com/en-us/help/5026717', '2023-05-11', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 4'), + (16, 4025, 'CU3', 'https://support.microsoft.com/en-us/help/5024396', '2023-04-13', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 3'), + (16, 4015, 'CU2', 'https://support.microsoft.com/en-us/help/5023127', '2023-03-15', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 2'), + (16, 4003, 'CU1', 'https://support.microsoft.com/en-us/help/5022375', '2023-02-16', '2028-01-11', '2033-01-11', 'SQL Server 2022', 'Cumulative Update 1'), + (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'), + (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'), + (15, 4390, 'CU28 GDR', 'https://support.microsoft.com/kb/5042749', '2024-09-10', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 28 GDR'), + (15, 4385, 'CU28', 'https://support.microsoft.com/kb/5039747', '2024-08-01', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 28'), + (15, 4375, 'CU27', 'https://support.microsoft.com/kb/5037331', '2024-06-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 27'), + (15, 4365, 'CU26', 'https://support.microsoft.com/kb/5035123', '2024-04-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 26'), + (15, 4355, 'CU25', 'https://support.microsoft.com/kb/5033688', '2024-02-15', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 25'), + (15, 4345, 'CU24', 'https://support.microsoft.com/kb/5031908', '2023-12-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 24'), + (15, 4335, 'CU23', 'https://support.microsoft.com/kb/5030333', '2023-10-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 23'), + (15, 4322, 'CU22', 'https://support.microsoft.com/kb/5027702', '2023-08-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 22'), + (15, 4316, 'CU21', 'https://support.microsoft.com/kb/5025808', '2023-06-15', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 21'), + (15, 4312, 'CU20', 'https://support.microsoft.com/kb/5024276', '2023-04-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 20'), + (15, 4298, 'CU19', 'https://support.microsoft.com/kb/5023049', '2023-02-16', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 19'), + (15, 4280, 'CU18 GDR', 'https://support.microsoft.com/kb/5021124', '2023-02-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 18 GDR'), + (15, 4261, 'CU18', 'https://support.microsoft.com/en-us/help/5017593', '2022-09-28', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 18'), + (15, 4249, 'CU17', 'https://support.microsoft.com/en-us/help/5016394', '2022-08-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 17'), + (15, 4236, 'CU16 GDR', 'https://support.microsoft.com/en-us/help/5014353', '2022-06-14', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 16 GDR'), + (15, 4223, 'CU16', 'https://support.microsoft.com/en-us/help/5011644', '2022-04-18', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 16'), + (15, 4198, 'CU15', 'https://support.microsoft.com/en-us/help/5008996', '2022-01-07', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 15'), + (15, 4188, 'CU14', 'https://support.microsoft.com/en-us/help/5007182', '2021-11-22', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 14'), + (15, 4178, 'CU13', 'https://support.microsoft.com/en-us/help/5005679', '2021-10-05', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 13'), + (15, 4153, 'CU12', 'https://support.microsoft.com/en-us/help/5004524', '2021-08-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 12'), + (15, 4138, 'CU11', 'https://support.microsoft.com/en-us/help/5003249', '2021-06-10', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 11'), + (15, 4123, 'CU10', 'https://support.microsoft.com/en-us/help/5001090', '2021-04-06', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 10'), + (15, 4102, 'CU9', 'https://support.microsoft.com/en-us/help/5000642', '2021-02-11', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 9 '), + (15, 4073, 'CU8 GDR', 'https://support.microsoft.com/en-us/help/4583459', '2021-01-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 8 GDR '), + (15, 4073, 'CU8', 'https://support.microsoft.com/en-us/help/4577194', '2020-10-01', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 8 '), + (15, 4063, 'CU7', 'https://support.microsoft.com/en-us/help/4570012', '2020-09-02', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 7 '), + (15, 4053, 'CU6', 'https://support.microsoft.com/en-us/help/4563110', '2020-08-04', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 6 '), + (15, 4043, 'CU5', 'https://support.microsoft.com/en-us/help/4548597', '2020-06-22', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 5 '), + (15, 4033, 'CU4', 'https://support.microsoft.com/en-us/help/4548597', '2020-03-31', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 4 '), + (15, 4023, 'CU3', 'https://support.microsoft.com/en-us/help/4538853', '2020-03-12', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 3 '), + (15, 4013, 'CU2', 'https://support.microsoft.com/en-us/help/4536075', '2020-02-13', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 2 '), + (15, 4003, 'CU1', 'https://support.microsoft.com/en-us/help/4527376', '2020-01-07', '2025-01-07', '2030-01-08', 'SQL Server 2019', 'Cumulative Update 1 '), + (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'), + (14, 3471, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5040940', '2024-07-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3465, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5029376', '2023-10-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3460, 'RTM CU31 GDR', 'https://support.microsoft.com/kb/5021126', '2023-02-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31 GDR'), + (14, 3456, 'RTM CU31', 'https://support.microsoft.com/en-us/help/5016884', '2022-09-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 31'), + (14, 3451, 'RTM CU30', 'https://support.microsoft.com/en-us/help/5013756', '2022-07-13', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 30'), + (14, 3445, 'RTM CU29 GDR', 'https://support.microsoft.com/en-us/help/5014553', '2022-06-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 29 GDR'), + (14, 3436, 'RTM CU29', 'https://support.microsoft.com/en-us/help/5010786', '2022-03-31', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 29'), + (14, 3430, 'RTM CU28', 'https://support.microsoft.com/en-us/help/5006944', '2022-01-13', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 28'), + (14, 3421, 'RTM CU27', 'https://support.microsoft.com/en-us/help/5006944', '2021-10-27', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 27'), + (14, 3411, 'RTM CU26', 'https://support.microsoft.com/en-us/help/5005226', '2021-09-14', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 26'), + (14, 3401, 'RTM CU25', 'https://support.microsoft.com/en-us/help/5003830', '2021-07-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 25'), + (14, 3391, 'RTM CU24', 'https://support.microsoft.com/en-us/help/5001228', '2021-05-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 24'), + (14, 3381, 'RTM CU23', 'https://support.microsoft.com/en-us/help/5000685', '2021-02-25', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 23'), + (14, 3370, 'RTM CU22 GDR', 'https://support.microsoft.com/en-us/help/4583457', '2021-01-12', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 22 GDR'), + (14, 3356, 'RTM CU22', 'https://support.microsoft.com/en-us/help/4577467', '2020-09-10', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 22'), + (14, 3335, 'RTM CU21', 'https://support.microsoft.com/en-us/help/4557397', '2020-07-01', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 21'), + (14, 3294, 'RTM CU20', 'https://support.microsoft.com/en-us/help/4541283', '2020-04-07', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 20'), + (14, 3257, 'RTM CU19', 'https://support.microsoft.com/en-us/help/4535007', '2020-02-05', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 19'), + (14, 3257, 'RTM CU18', 'https://support.microsoft.com/en-us/help/4527377', '2019-12-09', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 18'), + (14, 3238, 'RTM CU17', 'https://support.microsoft.com/en-us/help/4515579', '2019-10-08', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 17'), + (14, 3223, 'RTM CU16', 'https://support.microsoft.com/en-us/help/4508218', '2019-08-01', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 16'), + (14, 3162, 'RTM CU15', 'https://support.microsoft.com/en-us/help/4498951', '2019-05-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 15'), + (14, 3076, 'RTM CU14', 'https://support.microsoft.com/en-us/help/4484710', '2019-03-25', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 14'), + (14, 3048, 'RTM CU13', 'https://support.microsoft.com/en-us/help/4466404', '2018-12-18', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 13'), + (14, 3045, 'RTM CU12', 'https://support.microsoft.com/en-us/help/4464082', '2018-10-24', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 12'), + (14, 3038, 'RTM CU11', 'https://support.microsoft.com/en-us/help/4462262', '2018-09-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 11'), + (14, 3037, 'RTM CU10', 'https://support.microsoft.com/en-us/help/4524334', '2018-08-27', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 10'), + (14, 3030, 'RTM CU9', 'https://support.microsoft.com/en-us/help/4515435', '2018-07-18', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 9'), + (14, 3029, 'RTM CU8', 'https://support.microsoft.com/en-us/help/4338363', '2018-06-21', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 8'), + (14, 3026, 'RTM CU7', 'https://support.microsoft.com/en-us/help/4229789', '2018-05-23', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 7'), + (14, 3025, 'RTM CU6', 'https://support.microsoft.com/en-us/help/4101464', '2018-04-17', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 6'), + (14, 3023, 'RTM CU5', 'https://support.microsoft.com/en-us/help/4092643', '2018-03-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 5'), + (14, 3022, 'RTM CU4', 'https://support.microsoft.com/en-us/help/4056498', '2018-02-20', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 4'), + (14, 3015, 'RTM CU3', 'https://support.microsoft.com/en-us/help/4052987', '2018-01-04', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 3'), + (14, 3008, 'RTM CU2', 'https://support.microsoft.com/en-us/help/4052574', '2017-11-28', '2022-10-11', '2027-10-12', 'SQL Server 2017', 'RTM Cumulative Update 2'), + (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'), + (13, 7029, 'SP3 Azure Feature Pack GDR', 'https://support.microsoft.com/en-us/help/5029187', '2023-10-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 Azure Feature Pack GDR'), + (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'), + (13, 6441, 'SP3 GDR', 'https://support.microsoft.com/kb/5040946', '2024-07-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6435, 'SP3 GDR', 'https://support.microsoft.com/kb/5029186', '2023-10-10', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6430, 'SP3 GDR', 'https://support.microsoft.com/kb/5021129', '2023-02-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6419, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/5014355', '2022-06-14', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6404, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/5006943', '2021-10-27', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3 GDR'), + (13, 6300, 'SP3 ', 'https://support.microsoft.com/en-us/help/5003279', '2021-09-15', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 3'), + (13, 5888, 'SP2 CU17', 'https://support.microsoft.com/en-us/help/5001092', '2021-03-29', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 17'), + (13, 5882, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/5000645', '2021-02-11', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 16'), + (13, 5865, 'SP2 CU15 GDR', 'https://support.microsoft.com/en-us/help/4583461', '2021-01-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 15 GDR'), + (13, 5850, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/4577775', '2020-09-28', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 15'), + (13, 5830, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/4564903', '2020-08-06', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 14'), + (13, 5820, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/4549825', '2020-05-28', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 13'), + (13, 5698, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/4536648', '2020-02-25', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 12'), + (13, 5598, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/4527378', '2019-12-09', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 11'), + (13, 5492, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/4505830', '2019-10-08', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 10'), + (13, 5479, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/4505830', '2019-09-30', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 9'), + (13, 5426, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/4505830', '2019-07-31', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 8'), + (13, 5337, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/4495256', '2019-05-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 7'), + (13, 5292, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/4488536', '2019-03-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 6'), + (13, 5264, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/4475776', '2019-01-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 5'), + (13, 5233, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/4464106', '2018-11-13', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 4'), + (13, 5216, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/4458871', '2018-09-20', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 3'), + (13, 5201, 'SP2 CU2 + Security Update', 'https://support.microsoft.com/en-us/help/4458621', '2018-08-21', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 2 + Security Update'), + (13, 5153, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/4340355', '2018-07-16', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 2'), + (13, 5149, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/4135048', '2018-05-30', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 Cumulative Update 1'), + (13, 5103, 'SP2 GDR', 'https://support.microsoft.com/en-us/help/4583460', '2021-01-12', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 GDR'), + (13, 5026, 'SP2 ', 'https://support.microsoft.com/en-us/help/4052908', '2018-04-24', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 2 '), + (13, 4574, 'SP1 CU15', 'https://support.microsoft.com/en-us/help/4495257', '2019-05-16', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 15'), + (13, 4560, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/4488535', '2019-03-19', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 14'), + (13, 4550, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/4475775', '2019-01-23', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 13'), + (13, 4541, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/4464343', '2018-11-13', '2021-07-13', '2026-07-14', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 12'), + (13, 4528, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/4459676', '2018-09-17', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 11'), + (13, 4514, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/4341569', '2018-07-16', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 10'), + (13, 4502, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/4100997', '2018-05-30', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 9'), + (13, 4474, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/4077064', '2018-03-19', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 8'), + (13, 4466, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/4057119', '2018-01-04', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 7'), + (13, 4457, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/4037354', '2017-11-20', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 6'), + (13, 4451, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/4024305', '2017-09-18', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 5'), + (13, 4446, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/4024305', '2017-08-08', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 4'), + (13, 4435, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/4019916', '2017-05-15', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 3'), + (13, 4422, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/4013106', '2017-03-20', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 2'), + (13, 4411, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/3208177', '2017-01-17', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 1'), + (13, 4224, 'SP1 CU10 + Security Update', 'https://support.microsoft.com/en-us/help/4458842', '2018-08-22', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 Cumulative Update 10 + Security Update'), + (13, 4001, 'SP1 ', 'https://support.microsoft.com/en-us/help/3182545 ', '2016-11-16', '2019-07-09', '2019-07-09', 'SQL Server 2016', 'Service Pack 1 '), + (13, 2216, 'RTM CU9', 'https://support.microsoft.com/en-us/help/4037357', '2017-11-20', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 9'), + (13, 2213, 'RTM CU8', 'https://support.microsoft.com/en-us/help/4024304', '2017-09-18', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 8'), + (13, 2210, 'RTM CU7', 'https://support.microsoft.com/en-us/help/4024304', '2017-08-08', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 7'), + (13, 2204, 'RTM CU6', 'https://support.microsoft.com/en-us/help/4019914', '2017-05-15', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 6'), + (13, 2197, 'RTM CU5', 'https://support.microsoft.com/en-us/help/4013105', '2017-03-20', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 5'), + (13, 2193, 'RTM CU4', 'https://support.microsoft.com/en-us/help/3205052 ', '2017-01-17', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 4'), + (13, 2186, 'RTM CU3', 'https://support.microsoft.com/en-us/help/3205413 ', '2016-11-16', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 3'), + (13, 2164, 'RTM CU2', 'https://support.microsoft.com/en-us/help/3182270 ', '2016-09-22', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 2'), + (13, 2149, 'RTM CU1', 'https://support.microsoft.com/en-us/help/3164674 ', '2016-07-25', '2018-01-09', '2018-01-09', 'SQL Server 2016', 'RTM Cumulative Update 1'), + (13, 1601, 'RTM ', '', '2016-06-01', '2019-01-09', '2019-01-09', 'SQL Server 2016', 'RTM '), + /*2014*/ + (12, 6449, 'SP3 CU4 GDR', 'https://support.microsoft.com/kb/5029185', '2023-10-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6444, 'SP3 CU4 GDR', 'https://support.microsoft.com/kb/5021045', '2023-02-14', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6439, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/5014164', '2022-06-14', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6433, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/4583462', '2021-01-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6372, 'SP3 CU4 GDR', 'https://support.microsoft.com/en-us/help/4535288', '2020-02-11', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4 GDR'), + (12, 6329, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/4500181', '2019-07-29', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 4'), + (12, 6259, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/4491539', '2019-04-16', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 3'), + (12, 6214, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/4482960', '2019-02-19', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 2'), + (12, 6205, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/4470220', '2018-12-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 Cumulative Update 1'), + (12, 6164, 'SP3 GDR', 'https://support.microsoft.com/en-us/help/4583463', '2021-01-12', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 GDR'), + (12, 6024, 'SP3 ', 'https://support.microsoft.com/en-us/help/4022619', '2018-10-30', '2019-07-09', '2024-07-09', 'SQL Server 2014', 'Service Pack 3 '), + (12, 5687, 'SP2 CU18', 'https://support.microsoft.com/en-us/help/4500180', '2019-07-29', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 18'), + (12, 5632, 'SP2 CU17', 'https://support.microsoft.com/en-us/help/4491540', '2019-04-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 17'), + (12, 5626, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/4482967', '2019-02-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 16'), + (12, 5605, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/4469137', '2018-12-12', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 15'), + (12, 5600, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/4459860', '2018-10-15', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 14'), + (12, 5590, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/4456287', '2018-08-27', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 13'), + (12, 5589, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/4130489', '2018-06-18', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 12'), + (12, 5579, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/4077063', '2018-03-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 11'), + (12, 5571, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/4052725', '2018-01-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 10'), + (12, 5563, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/4055557', '2017-12-18', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 9'), + (12, 5557, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/4037356', '2017-10-16', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 8'), + (12, 5556, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/4032541', '2017-08-28', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 7'), + (12, 5553, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/4019094', '2017-08-08', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 6'), + (12, 5546, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/4013098', '2017-04-17', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 5'), + (12, 5540, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/4010394', '2017-02-21', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 4'), + (12, 5538, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/3204388 ', '2016-12-19', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 3'), + (12, 5522, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/3188778 ', '2016-10-17', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 2'), + (12, 5511, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/3178925 ', '2016-08-25', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 Cumulative Update 1'), + (12, 5000, 'SP2 ', 'https://support.microsoft.com/en-us/help/3171021 ', '2016-07-11', '2020-01-14', '2020-01-14', 'SQL Server 2014', 'Service Pack 2 '), + (12, 4522, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/4019099', '2017-08-08', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 13'), + (12, 4511, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/4017793', '2017-04-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 12'), + (12, 4502, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/4010392', '2017-02-21', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 11'), + (12, 4491, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/3204399 ', '2016-12-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 10'), + (12, 4474, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/3186964 ', '2016-10-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 9'), + (12, 4468, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/3174038 ', '2016-08-15', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 8'), + (12, 4459, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/3162659 ', '2016-06-20', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 7'), + (12, 4457, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/3167392 ', '2016-05-30', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 6'), + (12, 4449, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/3144524', '2016-04-18', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 6'), + (12, 4438, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/3130926', '2016-02-22', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 5'), + (12, 4436, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/3106660', '2015-12-21', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 4'), + (12, 4427, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/3094221', '2015-10-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 3'), + (12, 4422, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/3075950', '2015-08-17', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 2'), + (12, 4416, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/3067839', '2015-06-19', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 Cumulative Update 1'), + (12, 4213, 'SP1 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3070446', '2015-07-14', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 MS15-058: GDR Security Update'), + (12, 4100, 'SP1 ', 'https://support.microsoft.com/en-us/help/3058865', '2015-05-04', '2017-10-10', '2017-10-10', 'SQL Server 2014', 'Service Pack 1 '), + (12, 2569, 'RTM CU14', 'https://support.microsoft.com/en-us/help/3158271 ', '2016-06-20', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 14'), + (12, 2568, 'RTM CU13', 'https://support.microsoft.com/en-us/help/3144517', '2016-04-18', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 13'), + (12, 2564, 'RTM CU12', 'https://support.microsoft.com/en-us/help/3130923', '2016-02-22', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 12'), + (12, 2560, 'RTM CU11', 'https://support.microsoft.com/en-us/help/3106659', '2015-12-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 11'), + (12, 2556, 'RTM CU10', 'https://support.microsoft.com/en-us/help/3094220', '2015-10-19', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 10'), + (12, 2553, 'RTM CU9', 'https://support.microsoft.com/en-us/help/3075949', '2015-08-17', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 9'), + (12, 2548, 'RTM MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045323', '2015-07-14', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS15-058: QFE Security Update'), + (12, 2546, 'RTM CU8', 'https://support.microsoft.com/en-us/help/3067836', '2015-06-19', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 8'), + (12, 2495, 'RTM CU7', 'https://support.microsoft.com/en-us/help/3046038', '2015-04-20', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 7'), + (12, 2480, 'RTM CU6', 'https://support.microsoft.com/en-us/help/3031047', '2015-02-16', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 6'), + (12, 2456, 'RTM CU5', 'https://support.microsoft.com/en-us/help/3011055', '2014-12-17', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 5'), + (12, 2430, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2999197', '2014-10-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 4'), + (12, 2402, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2984923', '2014-08-18', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 3'), + (12, 2381, 'RTM MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2977316', '2014-08-12', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS14-044: QFE Security Update'), + (12, 2370, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2967546', '2014-06-27', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 2'), + (12, 2342, 'RTM CU1', 'https://support.microsoft.com/en-us/help/2931693', '2014-04-21', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM Cumulative Update 1'), + (12, 2269, 'RTM MS15-058: GDR Security Update ', 'https://support.microsoft.com/en-us/help/3045324', '2015-07-14', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS15-058: GDR Security Update '), + (12, 2254, 'RTM MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977315', '2014-08-12', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM MS14-044: GDR Security Update'), + (12, 2000, 'RTM ', '', '2014-04-01', '2016-07-12', '2016-07-12', 'SQL Server 2014', 'RTM '), + /*2012*/ + (11, 7512, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/5021123', '2023-02-16', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2021-1636'), + (11, 7507, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/4583465', '2021-01-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2021-1636'), + (11, 7493, 'SP4 GDR Security Update', 'https://support.microsoft.com/en-us/help/4532098', '2020-02-11', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 GDR Security Update for CVE-2020-0618'), + (11, 7469, 'SP4 On-Demand Hotfix Update', 'https://support.microsoft.com/en-us/help/4091266', '2018-03-28', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 SP4 On-Demand Hotfix Update'), + (11, 7462, 'SP4 ADV180002: GDR Security Update', 'https://support.microsoft.com/en-us/help/4057116', '2018-01-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 ADV180002: GDR Security Update'), + (11, 7001, 'SP4 ', 'https://support.microsoft.com/en-us/help/4018073', '2017-10-02', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'Service Pack 4 '), + (11, 6607, 'SP3 CU10', 'https://support.microsoft.com/en-us/help/4025925', '2017-08-08', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 10'), + (11, 6598, 'SP3 CU9', 'https://support.microsoft.com/en-us/help/4016762', '2017-05-15', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 9'), + (11, 6594, 'SP3 CU8', 'https://support.microsoft.com/en-us/help/3205051 ', '2017-03-20', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 8'), + (11, 6579, 'SP3 CU7', 'https://support.microsoft.com/en-us/help/3205051 ', '2017-01-17', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 7'), + (11, 6567, 'SP3 CU6', 'https://support.microsoft.com/en-us/help/3194992 ', '2016-11-17', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 6'), + (11, 6544, 'SP3 CU5', 'https://support.microsoft.com/en-us/help/3180915 ', '2016-09-19', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 5'), + (11, 6540, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/3165264 ', '2016-07-18', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 4'), + (11, 6537, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/3152635 ', '2016-05-16', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 3'), + (11, 6523, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/3137746', '2016-03-21', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 2'), + (11, 6518, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/3123299', '2016-01-19', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 Cumulative Update 1'), + (11, 6020, 'SP3 ', 'https://support.microsoft.com/en-us/help/3072779', '2015-11-20', '2018-10-09', '2018-10-09', 'SQL Server 2012', 'Service Pack 3 '), + (11, 5678, 'SP2 CU16', 'https://support.microsoft.com/en-us/help/3205416 ', '2016-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 16'), + (11, 5676, 'SP2 CU15', 'https://support.microsoft.com/en-us/help/3205416 ', '2016-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 15'), + (11, 5657, 'SP2 CU14', 'https://support.microsoft.com/en-us/help/3180914 ', '2016-09-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 14'), + (11, 5655, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/3165266 ', '2016-07-18', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 13'), + (11, 5649, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/3152637 ', '2016-05-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 12'), + (11, 5646, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/3137745', '2016-03-21', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 11'), + (11, 5644, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/3120313', '2016-01-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 10'), + (11, 5641, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/3098512', '2015-11-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 9'), + (11, 5634, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/3082561', '2015-09-21', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 8'), + (11, 5623, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/3072100', '2015-07-20', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 7'), + (11, 5613, 'SP2 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045319', '2015-07-14', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 MS15-058: QFE Security Update'), + (11, 5592, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/3052468', '2015-05-18', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 6'), + (11, 5582, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/3037255', '2015-03-16', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 5'), + (11, 5569, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/3007556', '2015-01-19', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 4'), + (11, 5556, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/3002049', '2014-11-17', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 3'), + (11, 5548, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2983175', '2014-09-15', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 2'), + (11, 5532, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2976982', '2014-07-23', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 Cumulative Update 1'), + (11, 5343, 'SP2 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045321', '2015-07-14', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 MS15-058: GDR Security Update'), + (11, 5058, 'SP2 ', 'https://support.microsoft.com/en-us/help/2958429', '2014-06-10', '2017-01-10', '2017-01-10', 'SQL Server 2012', 'Service Pack 2 '), + (11, 3513, 'SP1 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045317', '2015-07-14', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS15-058: QFE Security Update'), + (11, 3482, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/3002044', '2014-11-17', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 13'), + (11, 3470, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2991533', '2014-09-15', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 12'), + (11, 3460, 'SP1 MS14-044: QFE Security Update ', 'https://support.microsoft.com/en-us/help/2977325', '2014-08-12', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS14-044: QFE Security Update '), + (11, 3449, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2975396', '2014-07-21', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 11'), + (11, 3431, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2954099', '2014-05-19', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 10'), + (11, 3412, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2931078', '2014-03-17', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 9'), + (11, 3401, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/2917531', '2014-01-20', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 8'), + (11, 3393, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/2894115', '2013-11-18', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 7'), + (11, 3381, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/2874879', '2013-09-16', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 6'), + (11, 3373, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/2861107', '2013-07-15', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 5'), + (11, 3368, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/2833645', '2013-05-30', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 4'), + (11, 3349, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/2812412', '2013-03-18', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 3'), + (11, 3339, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/2790947', '2013-01-21', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 2'), + (11, 3321, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/2765331', '2012-11-20', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 Cumulative Update 1'), + (11, 3156, 'SP1 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045318', '2015-07-14', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS15-058: GDR Security Update'), + (11, 3153, 'SP1 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977326', '2014-08-12', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 MS14-044: GDR Security Update'), + (11, 3000, 'SP1 ', 'https://support.microsoft.com/en-us/help/2674319', '2012-11-07', '2015-07-14', '2015-07-14', 'SQL Server 2012', 'Service Pack 1 '), + (11, 2424, 'RTM CU11', 'https://support.microsoft.com/en-us/help/2908007', '2013-12-16', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 11'), + (11, 2420, 'RTM CU10', 'https://support.microsoft.com/en-us/help/2891666', '2013-10-21', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 10'), + (11, 2419, 'RTM CU9', 'https://support.microsoft.com/en-us/help/2867319', '2013-08-20', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 9'), + (11, 2410, 'RTM CU8', 'https://support.microsoft.com/en-us/help/2844205', '2013-06-17', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 8'), + (11, 2405, 'RTM CU7', 'https://support.microsoft.com/en-us/help/2823247', '2013-04-15', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 7'), + (11, 2401, 'RTM CU6', 'https://support.microsoft.com/en-us/help/2728897', '2013-02-18', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 6'), + (11, 2395, 'RTM CU5', 'https://support.microsoft.com/en-us/help/2777772', '2012-12-17', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 5'), + (11, 2383, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2758687', '2012-10-15', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 4'), + (11, 2376, 'RTM MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716441', '2012-10-09', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM MS12-070: QFE Security Update'), + (11, 2332, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2723749', '2012-08-31', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 3'), + (11, 2325, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2703275', '2012-06-18', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 2'), + (11, 2316, 'RTM CU1', 'https://support.microsoft.com/en-us/help/2679368', '2012-04-12', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM Cumulative Update 1'), + (11, 2218, 'RTM MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716442', '2012-10-09', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM MS12-070: GDR Security Update'), + (11, 2100, 'RTM ', '', '2012-03-06', '2017-07-11', '2022-07-12', 'SQL Server 2012', 'RTM '), + /*2008 R2*/ + (10, 6529, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045314', '2015-07-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6220, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045316', '2015-07-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6000, 'SP3 ', 'https://support.microsoft.com/en-us/help/2979597', '2014-09-26', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'Service Pack 3 '), + (10, 4339, 'SP2 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045312', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS15-058: QFE Security Update'), + (10, 4321, 'SP2 MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2977319', '2014-08-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS14-044: QFE Security Update'), + (10, 4319, 'SP2 CU13', 'https://support.microsoft.com/en-us/help/2967540', '2014-06-30', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 13'), + (10, 4305, 'SP2 CU12', 'https://support.microsoft.com/en-us/help/2938478', '2014-04-21', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 12'), + (10, 4302, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/2926028', '2014-02-18', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 11'), + (10, 4297, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/2908087', '2013-12-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 10'), + (10, 4295, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/2887606', '2013-10-28', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 9'), + (10, 4290, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/2871401', '2013-08-22', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 8'), + (10, 4285, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/2844090', '2013-06-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 7'), + (10, 4279, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/2830140', '2013-04-15', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 6'), + (10, 4276, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/2797460', '2013-02-18', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 5'), + (10, 4270, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/2777358', '2012-12-17', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 4'), + (10, 4266, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/2754552', '2012-10-15', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 3'), + (10, 4263, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2740411', '2012-08-31', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 2'), + (10, 4260, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2720425', '2012-07-24', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 Cumulative Update 1'), + (10, 4042, 'SP2 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045313', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS15-058: GDR Security Update'), + (10, 4033, 'SP2 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977320', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 MS14-044: GDR Security Update'), + (10, 4000, 'SP2 ', 'https://support.microsoft.com/en-us/help/2630458', '2012-07-26', '2015-10-13', '2015-10-13', 'SQL Server 2008 R2', 'Service Pack 2 '), + (10, 2881, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/2868244', '2013-08-08', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 14'), + (10, 2876, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/2855792', '2013-06-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 13'), + (10, 2874, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2828727', '2013-04-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 12'), + (10, 2869, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2812683', '2013-02-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 11'), + (10, 2868, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2783135', '2012-12-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 10'), + (10, 2866, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2756574', '2012-10-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 9'), + (10, 2861, 'SP1 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716439', '2012-10-09', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 MS12-070: QFE Security Update'), + (10, 2822, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/2723743', '2012-08-31', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 8'), + (10, 2817, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/2703282', '2012-06-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 7'), + (10, 2811, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/2679367', '2012-04-16', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 6'), + (10, 2806, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/2659694', '2012-02-22', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 5'), + (10, 2796, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/2633146', '2011-12-19', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 4'), + (10, 2789, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/2591748', '2011-10-17', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 3'), + (10, 2772, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/2567714', '2011-08-15', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 2'), + (10, 2769, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/2544793', '2011-07-18', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 Cumulative Update 1'), + (10, 2550, 'SP1 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2754849', '2012-10-09', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 MS12-070: GDR Security Update'), + (10, 2500, 'SP1 ', 'https://support.microsoft.com/en-us/help/2528583', '2011-07-12', '2013-10-08', '2013-10-08', 'SQL Server 2008 R2', 'Service Pack 1 '), + (10, 1815, 'RTM CU13', 'https://support.microsoft.com/en-us/help/2679366', '2012-04-16', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 13'), + (10, 1810, 'RTM CU12', 'https://support.microsoft.com/en-us/help/2659692', '2012-02-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 12'), + (10, 1809, 'RTM CU11', 'https://support.microsoft.com/en-us/help/2633145', '2011-12-19', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 11'), + (10, 1807, 'RTM CU10', 'https://support.microsoft.com/en-us/help/2591746', '2011-10-17', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 10'), + (10, 1804, 'RTM CU9', 'https://support.microsoft.com/en-us/help/2567713', '2011-08-15', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 9'), + (10, 1797, 'RTM CU8', 'https://support.microsoft.com/en-us/help/2534352', '2011-06-20', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 8'), + (10, 1790, 'RTM MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494086', '2011-06-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM MS11-049: QFE Security Update'), + (10, 1777, 'RTM CU7', 'https://support.microsoft.com/en-us/help/2507770', '2011-04-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 7'), + (10, 1765, 'RTM CU6', 'https://support.microsoft.com/en-us/help/2489376', '2011-02-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 6'), + (10, 1753, 'RTM CU5', 'https://support.microsoft.com/en-us/help/2438347', '2010-12-20', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 5'), + (10, 1746, 'RTM CU4', 'https://support.microsoft.com/en-us/help/2345451', '2010-10-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 4'), + (10, 1734, 'RTM CU3', 'https://support.microsoft.com/en-us/help/2261464', '2010-08-16', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 3'), + (10, 1720, 'RTM CU2', 'https://support.microsoft.com/en-us/help/2072493', '2010-06-21', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 2'), + (10, 1702, 'RTM CU1', 'https://support.microsoft.com/en-us/help/981355', '2010-05-18', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM Cumulative Update 1'), + (10, 1617, 'RTM MS11-049: GDR Security Update', 'https://support.microsoft.com/en-us/help/2494088', '2011-06-14', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM MS11-049: GDR Security Update'), + (10, 1600, 'RTM ', '', '2010-05-10', '2014-07-08', '2019-07-09', 'SQL Server 2008 R2', 'RTM '), + /*2008*/ + (10, 6535, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045308', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 6241, 'SP3 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045311', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: GDR Security Update'), + (10, 5890, 'SP3 MS15-058: QFE Security Update', 'https://support.microsoft.com/en-us/help/3045303', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: QFE Security Update'), + (10, 5869, 'SP3 MS14-044: QFE Security Update', 'https://support.microsoft.com/en-us/help/2984340, https://support.microsoft.com/en-us/help/2977322', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS14-044: QFE Security Update'), + (10, 5861, 'SP3 CU17', 'https://support.microsoft.com/en-us/help/2958696', '2014-05-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 17'), + (10, 5852, 'SP3 CU16', 'https://support.microsoft.com/en-us/help/2936421', '2014-03-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 16'), + (10, 5850, 'SP3 CU15', 'https://support.microsoft.com/en-us/help/2923520', '2014-01-20', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 15'), + (10, 5848, 'SP3 CU14', 'https://support.microsoft.com/en-us/help/2893410', '2013-11-18', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 14'), + (10, 5846, 'SP3 CU13', 'https://support.microsoft.com/en-us/help/2880350', '2013-09-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 13'), + (10, 5844, 'SP3 CU12', 'https://support.microsoft.com/en-us/help/2863205', '2013-07-15', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 12'), + (10, 5840, 'SP3 CU11', 'https://support.microsoft.com/en-us/help/2834048', '2013-05-20', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 11'), + (10, 5835, 'SP3 CU10', 'https://support.microsoft.com/en-us/help/2814783', '2013-03-18', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 10'), + (10, 5829, 'SP3 CU9', 'https://support.microsoft.com/en-us/help/2799883', '2013-01-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 9'), + (10, 5828, 'SP3 CU8', 'https://support.microsoft.com/en-us/help/2771833', '2012-11-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 8'), + (10, 5826, 'SP3 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716435', '2012-10-09', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS12-070: QFE Security Update'), + (10, 5794, 'SP3 CU7', 'https://support.microsoft.com/en-us/help/2738350', '2012-09-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 7'), + (10, 5788, 'SP3 CU6', 'https://support.microsoft.com/en-us/help/2715953', '2012-07-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 6'), + (10, 5785, 'SP3 CU5', 'https://support.microsoft.com/en-us/help/2696626', '2012-05-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 5'), + (10, 5775, 'SP3 CU4', 'https://support.microsoft.com/en-us/help/2673383', '2012-03-19', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 4'), + (10, 5770, 'SP3 CU3', 'https://support.microsoft.com/en-us/help/2648098', '2012-01-16', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 3'), + (10, 5768, 'SP3 CU2', 'https://support.microsoft.com/en-us/help/2633143', '2011-11-21', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 2'), + (10, 5766, 'SP3 CU1', 'https://support.microsoft.com/en-us/help/2617146', '2011-10-17', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 Cumulative Update 1'), + (10, 5538, 'SP3 MS15-058: GDR Security Update', 'https://support.microsoft.com/en-us/help/3045305', '2015-07-14', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS15-058: GDR Security Update'), + (10, 5520, 'SP3 MS14-044: GDR Security Update', 'https://support.microsoft.com/en-us/help/2977321', '2014-08-12', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS14-044: GDR Security Update'), + (10, 5512, 'SP3 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716436', '2012-10-09', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 MS12-070: GDR Security Update'), + (10, 5500, 'SP3 ', 'https://support.microsoft.com/en-us/help/2546951', '2011-10-06', '2015-10-13', '2015-10-13', 'SQL Server 2008', 'Service Pack 3 '), + (10, 4371, 'SP2 MS12-070: QFE Security Update', 'https://support.microsoft.com/en-us/help/2716433', '2012-10-09', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS12-070: QFE Security Update'), + (10, 4333, 'SP2 CU11', 'https://support.microsoft.com/en-us/help/2715951', '2012-07-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 11'), + (10, 4332, 'SP2 CU10', 'https://support.microsoft.com/en-us/help/2696625', '2012-05-21', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 10'), + (10, 4330, 'SP2 CU9', 'https://support.microsoft.com/en-us/help/2673382', '2012-03-19', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 9'), + (10, 4326, 'SP2 CU8', 'https://support.microsoft.com/en-us/help/2648096', '2012-01-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 8'), + (10, 4323, 'SP2 CU7', 'https://support.microsoft.com/en-us/help/2617148', '2011-11-21', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 7'), + (10, 4321, 'SP2 CU6', 'https://support.microsoft.com/en-us/help/2582285', '2011-09-19', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 6'), + (10, 4316, 'SP2 CU5', 'https://support.microsoft.com/en-us/help/2555408', '2011-07-18', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 5'), + (10, 4311, 'SP2 MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494094', '2011-06-14', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS11-049: QFE Security Update'), + (10, 4285, 'SP2 CU4', 'https://support.microsoft.com/en-us/help/2527180', '2011-05-16', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 4'), + (10, 4279, 'SP2 CU3', 'https://support.microsoft.com/en-us/help/2498535', '2011-03-17', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 3'), + (10, 4272, 'SP2 CU2', 'https://support.microsoft.com/en-us/help/2467239', '2011-01-17', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 2'), + (10, 4266, 'SP2 CU1', 'https://support.microsoft.com/en-us/help/2289254', '2010-11-15', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 Cumulative Update 1'), + (10, 4067, 'SP2 MS12-070: GDR Security Update', 'https://support.microsoft.com/en-us/help/2716434', '2012-10-09', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS12-070: GDR Security Update'), + (10, 4064, 'SP2 MS11-049: GDR Security Update', 'https://support.microsoft.com/en-us/help/2494089', '2011-06-14', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 MS11-049: GDR Security Update'), + (10, 4000, 'SP2 ', 'https://support.microsoft.com/en-us/help/2285068', '2010-09-29', '2012-10-09', '2012-10-09', 'SQL Server 2008', 'Service Pack 2 '), + (10, 2850, 'SP1 CU16', 'https://support.microsoft.com/en-us/help/2582282', '2011-09-19', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 16'), + (10, 2847, 'SP1 CU15', 'https://support.microsoft.com/en-us/help/2555406', '2011-07-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 15'), + (10, 2841, 'SP1 MS11-049: QFE Security Update', 'https://support.microsoft.com/en-us/help/2494100', '2011-06-14', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 MS11-049: QFE Security Update'), + (10, 2821, 'SP1 CU14', 'https://support.microsoft.com/en-us/help/2527187', '2011-05-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 14'), + (10, 2816, 'SP1 CU13', 'https://support.microsoft.com/en-us/help/2497673', '2011-03-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 13'), + (10, 2808, 'SP1 CU12', 'https://support.microsoft.com/en-us/help/2467236', '2011-01-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 12'), + (10, 2804, 'SP1 CU11', 'https://support.microsoft.com/en-us/help/2413738', '2010-11-15', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 11'), + (10, 2799, 'SP1 CU10', 'https://support.microsoft.com/en-us/help/2279604', '2010-09-20', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 10'), + (10, 2789, 'SP1 CU9', 'https://support.microsoft.com/en-us/help/2083921', '2010-07-19', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 9'), + (10, 2775, 'SP1 CU8', 'https://support.microsoft.com/en-us/help/981702', '2010-05-17', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 8'), + (10, 2766, 'SP1 CU7', 'https://support.microsoft.com/en-us/help/979065', '2010-03-26', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 7'), + (10, 2757, 'SP1 CU6', 'https://support.microsoft.com/en-us/help/977443', '2010-01-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 6'), + (10, 2746, 'SP1 CU5', 'https://support.microsoft.com/en-us/help/975977', '2009-11-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 5'), + (10, 2734, 'SP1 CU4', 'https://support.microsoft.com/en-us/help/973602', '2009-09-21', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 4'), + (10, 2723, 'SP1 CU3', 'https://support.microsoft.com/en-us/help/971491', '2009-07-20', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 3'), + (10, 2714, 'SP1 CU2', 'https://support.microsoft.com/en-us/help/970315', '2009-05-18', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 2'), + (10, 2710, 'SP1 CU1', 'https://support.microsoft.com/en-us/help/969099', '2009-04-16', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 Cumulative Update 1'), + (10, 2573, 'SP1 MS11-049: GDR Security update', 'https://support.microsoft.com/en-us/help/2494096', '2011-06-14', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 MS11-049: GDR Security update'), + (10, 2531, 'SP1 ', '', '2009-04-01', '2011-10-11', '2011-10-11', 'SQL Server 2008', 'Service Pack 1 '), + (10, 1835, 'RTM CU10', 'https://support.microsoft.com/en-us/help/979064', '2010-03-15', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 10'), + (10, 1828, 'RTM CU9', 'https://support.microsoft.com/en-us/help/977444', '2010-01-18', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 9'), + (10, 1823, 'RTM CU8', 'https://support.microsoft.com/en-us/help/975976', '2009-11-16', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 8'), + (10, 1818, 'RTM CU7', 'https://support.microsoft.com/en-us/help/973601', '2009-09-21', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 7'), + (10, 1812, 'RTM CU6', 'https://support.microsoft.com/en-us/help/971490', '2009-07-20', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 6'), + (10, 1806, 'RTM CU5', 'https://support.microsoft.com/en-us/help/969531', '2009-05-18', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 5'), + (10, 1798, 'RTM CU4', 'https://support.microsoft.com/en-us/help/963036', '2009-03-16', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 4'), + (10, 1787, 'RTM CU3', 'https://support.microsoft.com/en-us/help/960484', '2009-01-19', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 3'), + (10, 1779, 'RTM CU2', 'https://support.microsoft.com/en-us/help/958186', '2008-11-19', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 2'), + (10, 1763, 'RTM CU1', 'https://support.microsoft.com/en-us/help/956717', '2008-09-22', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM Cumulative Update 1'), + (10, 1600, 'RTM ', '', '2008-08-06', '2014-07-08', '2019-07-09', 'SQL Server 2008', 'RTM ') +; +GO diff --git a/Uninstall.sql b/Uninstall.sql new file mode 100644 index 000000000..f33b68851 --- /dev/null +++ b/Uninstall.sql @@ -0,0 +1,89 @@ +--First Responder Kit Uninstaller Script + +--Configuration Parameters + +DECLARE @allDatabases bit = 0; --Flip this bit to 1 if you want to uninstall the scripts from all the databases, not only the current one +DECLARE @printOnly bit = 0; --Flip this bit to 1 if you want to print the drop commands only without executing + +--End Configuration +--Variables + +SET NOCOUNT ON; +DECLARE @SQL nvarchar(max) = N''; + +IF OBJECT_ID('tempdb.dbo.#ToDelete') IS NOT NULL + DROP TABLE #ToDelete; + +SELECT 'sp_AllNightLog' as ProcedureName INTO #ToDelete UNION +SELECT 'sp_AllNightLog_Setup' as ProcedureName UNION +SELECT 'sp_Blitz' as ProcedureName UNION +SELECT 'sp_BlitzAnalysis' as ProcedureName UNION +SELECT 'sp_BlitzBackups' as ProcedureName UNION +SELECT 'sp_BlitzCache' as ProcedureName UNION +SELECT 'sp_BlitzFirst' as ProcedureName UNION +SELECT 'sp_BlitzInMemoryOLTP' as ProcedureName UNION +SELECT 'sp_BlitzIndex' as ProcedureName UNION +SELECT 'sp_BlitzLock' as ProcedureName UNION +SELECT 'sp_BlitzQueryStore' as ProcedureName UNION +SELECT 'sp_BlitzWho' as ProcedureName UNION +SELECT 'sp_DatabaseRestore' as ProcedureName UNION +SELECT 'sp_foreachdb' as ProcedureName UNION +SELECT 'sp_ineachdb' as ProcedureName + +--End Variables + +IF (@allDatabases = 0) +BEGIN + + SELECT @SQL += N'DROP PROCEDURE dbo.' + D.ProcedureName + ';' + CHAR(10) + FROM sys.procedures P + JOIN #ToDelete D ON D.ProcedureName = P.name COLLATE DATABASE_DEFAULT; + + SELECT @SQL += N'DROP TABLE dbo.SqlServerVersions;' + CHAR(10) + FROM sys.tables + WHERE schema_id = 1 AND name = 'SqlServerVersions'; + +END +ELSE +BEGIN + + DECLARE @dbname SYSNAME; + DECLARE @innerSQL NVARCHAR(max); + + DECLARE c CURSOR LOCAL FAST_FORWARD + FOR SELECT QUOTENAME([name]) + FROM sys.databases + WHERE [state] = 0; + + OPEN c; + + FETCH NEXT FROM c INTO @dbname; + + WHILE(@@FETCH_STATUS = 0) + BEGIN + + SET @innerSQL = N' SELECT @SQL += N''USE ' + @dbname + N';' + NCHAR(10) + N'DROP PROCEDURE dbo.'' + D.ProcedureName + '';'' + NCHAR(10) + FROM ' + @dbname + N'.sys.procedures P + JOIN #ToDelete D ON D.ProcedureName = P.name COLLATE DATABASE_DEFAULT'; + + EXEC sp_executesql @innerSQL, N'@SQL nvarchar(max) OUTPUT', @SQL = @SQL OUTPUT; + + SET @innerSQL = N' SELECT @SQL += N''USE ' + @dbname + N';' + NCHAR(10) + N'DROP TABLE dbo.SqlServerVersions;'' + NCHAR(10) + FROM ' + @dbname + N'.sys.tables + WHERE schema_id = 1 AND name = ''SqlServerVersions'''; + + EXEC sp_executesql @innerSQL, N'@SQL nvarchar(max) OUTPUT', @SQL = @SQL OUTPUT; + + FETCH NEXT FROM c INTO @dbname; + + END + + CLOSE c; + DEALLOCATE c; + +END + +PRINT @SQL; + +IF(@printOnly = 0) + EXEC sp_executesql @SQL diff --git a/sp_Blitz.sql b/sp_Blitz.sql old mode 100755 new mode 100644 index ed1d79da9..ca6eb2637 --- a/sp_Blitz.sql +++ b/sp_Blitz.sql @@ -25,18 +25,31 @@ ALTER PROCEDURE [dbo].[sp_Blitz] @EmailProfile sysname = NULL , @SummaryMode TINYINT = 0 , @BringThePain TINYINT = 0 , - @Debug TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT + @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, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; + SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; + + + SELECT @Version = '8.26', @VersionDate = '20251002'; SET @OutputType = UPPER(@OutputType); - IF @Help = 1 PRINT ' + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; + + IF @Help = 1 + BEGIN + PRINT ' /* sp_Blitz from http://FirstResponderKit.org @@ -63,7 +76,6 @@ AS Changes - for the full list of improvements and fixes in this version, see: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - Parameter explanations: @CheckUserDatabaseObjects 1=review user databases for triggers, heaps, etc. Takes more time for more databases and objects. @@ -71,22 +83,21 @@ AS @CheckProcedureCache 1=top 20-50 resource-intensive cache plans and analyze them for common performance issues. @OutputProcedureCache 1=output the top 20-50 resource-intensive plans even if they did not trigger an alarm @CheckProcedureCacheFilter ''CPU'' | ''Reads'' | ''Duration'' | ''ExecCount'' - @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list | ''SCHEMA''=version and field list | ''NONE'' = none + @OutputType ''TABLE''=table | ''COUNT''=row with number found | ''MARKDOWN''=bulleted list (including server info, excluding security findings) | ''SCHEMA''=version and field list | ''XML'' =table output as XML | ''NONE'' = none @IgnorePrioritiesBelow 50=ignore priorities below 50 @IgnorePrioritiesAbove 50=ignore priorities above 50 + @Debug 0=silent (Default) | 1=messages per step | 2=outputs dynamic queries For the rest of the parameters, see https://www.BrentOzar.com/blitz/documentation for details. - - MIT License - Copyright for portions of sp_Blitz are held by Microsoft as part of project + Copyright for portions of sp_Blitz are held by Microsoft as part of project tigertoolbox and are provided under the MIT license: https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. + + All other copyrights for sp_Blitz are held by Brent Ozar Unlimited. - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -106,16 +117,16 @@ AS OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - - */'; + RETURN; + END; /* @Help = 1 */ + ELSE IF @OutputType = 'SCHEMA' BEGIN SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [DatabaseName] NVARCHAR(128), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [QueryPlan] NVARCHAR(MAX), [QueryPlanFiltered] NVARCHAR(MAX), [CheckID] INT'; - END; - ELSE /* IF @OutputType = 'SCHEMA' */ + END;/* IF @OutputType = 'SCHEMA' */ + ELSE BEGIN DECLARE @StringToExecute NVARCHAR(4000) @@ -145,14 +156,313 @@ AS ,@MinServerMemory bigint ,@MaxServerMemory bigint ,@ColumnStoreIndexesInUse bit + ,@QueryStoreInUse bit ,@TraceFileIssue bit -- Flag for Windows OS to help with Linux support - ,@IsWindowsOperatingSystem BIT; + ,@IsWindowsOperatingSystem BIT + ,@DaysUptime NUMERIC(23,2) + /* For First Responder Kit consistency check:*/ + ,@spBlitzFullName VARCHAR(1024) + ,@BlitzIsOutdatedComparedToOthers BIT + ,@tsql NVARCHAR(MAX) + ,@VersionCheckModeExistsTSQL NVARCHAR(MAX) + ,@BlitzProcDbName VARCHAR(256) + ,@ExecRet INT + ,@InnerExecRet INT + ,@TmpCnt INT + ,@PreviousComponentName VARCHAR(256) + ,@PreviousComponentFullPath VARCHAR(1024) + ,@CurrentStatementId INT + ,@CurrentComponentSchema VARCHAR(256) + ,@CurrentComponentName VARCHAR(256) + ,@CurrentComponentType VARCHAR(256) + ,@CurrentComponentVersionDate DATETIME2 + ,@CurrentComponentFullName VARCHAR(1024) + ,@CurrentComponentMandatory BIT + ,@MaximumVersionDate DATETIME + ,@StatementCheckName VARCHAR(256) + ,@StatementOutputsCounter BIT + ,@OutputCounterExpectedValue INT + ,@StatementOutputsExecRet BIT + ,@StatementOutputsDateTime BIT + ,@CurrentComponentMandatoryCheckOK BIT + ,@CurrentComponentVersionCheckModeOK BIT + ,@canExitLoop BIT + ,@frkIsConsistent BIT + ,@NeedToTurnNumericRoundabortBackOn BIT + ,@sa bit = 1 + ,@SUSER_NAME sysname = SUSER_SNAME() + ,@SkipDBCC bit = 0 + ,@SkipTrace bit = 0 + ,@SkipXPRegRead bit = 0 + ,@SkipXPFixedDrives bit = 0 + ,@SkipXPCMDShell bit = 0 + ,@SkipMaster bit = 0 + ,@SkipMSDB_objs bit = 0 + ,@SkipMSDB_jobs bit = 0 + ,@SkipModel bit = 0 + ,@SkipTempDB bit = 0 + ,@SkipValidateLogins bit = 0 + ,@SkipGetAlertInfo bit = 0 + + DECLARE + @db_perms table + ( + database_name sysname, + permission_name sysname + ); + + INSERT + @db_perms + ( + database_name, + permission_name + ) + SELECT + database_name = + DB_NAME(d.database_id), + fmp.permission_name + FROM sys.databases AS d + CROSS APPLY fn_my_permissions(d.name, 'DATABASE') AS fmp + WHERE fmp.permission_name = N'SELECT'; /*Databases where we don't have read permissions*/ + + /* End of declarations for First Responder Kit consistency check:*/ + ; + + /* Create temp table for check 73 */ + IF OBJECT_ID('tempdb..#AlertInfo') IS NOT NULL + EXEC sp_executesql N'DROP TABLE #AlertInfo;'; + + CREATE TABLE #AlertInfo + ( + FailSafeOperator NVARCHAR(255) , + NotificationMethod INT , + ForwardingServer NVARCHAR(255) , + ForwardingSeverity INT , + PagerToTemplate NVARCHAR(255) , + PagerCCTemplate NVARCHAR(255) , + PagerSubjectTemplate NVARCHAR(255) , + PagerSendSubjectOnly NVARCHAR(255) , + ForwardAlways INT + ); + + /* Create temp table for check 2301 */ + IF OBJECT_ID('tempdb..#InvalidLogins') IS NOT NULL + EXEC sp_executesql N'DROP TABLE #InvalidLogins;'; + + CREATE TABLE #InvalidLogins + ( + LoginSID varbinary(85), + LoginName VARCHAR(256) + ); + + /*Starting permissions checks here, but only if we're not a sysadmin*/ + IF + ( + SELECT + sa = + ISNULL + ( + IS_SRVROLEMEMBER(N'sysadmin'), + 0 + ) + ) = 0 + BEGIN + IF @Debug IN (1, 2) RAISERROR('User not SA, checking permissions', 0, 1) WITH NOWAIT; + + SET @sa = 0; /*Setting this to 0 to skip DBCC COMMANDS*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.fn_my_permissions(NULL, NULL) AS fmp + WHERE fmp.permission_name = N'VIEW SERVER STATE' + ) + BEGIN + RAISERROR('The user %s does not have VIEW SERVER STATE permissions.', 0, 11, @SUSER_NAME) WITH NOWAIT; + RETURN; + END; /*If we don't have this, we can't do anything at all.*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(NULL, NULL) AS fmp + WHERE fmp.permission_name = N'ALTER TRACE' + ) + BEGIN + SET @SkipTrace = 1; + END; /*We need this permission to execute trace stuff, apparently*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'xp_fixeddrives', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipXPFixedDrives = 1; + END; /*Need execute on xp_fixeddrives*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'xp_cmdshell', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipXPCMDShell = 1; + END; /*Need execute on xp_cmdshell*/ + + IF ISNULL(@SkipValidateLogins, 0) != 1 /*If @SkipValidateLogins hasn't been set to 1 by the caller*/ + BEGIN + BEGIN TRY + /* Try to fill the table for check 2301 */ + INSERT INTO #InvalidLogins + ( + [LoginSID] + ,[LoginName] + ) + EXEC sp_validatelogins; + + SET @SkipValidateLogins = 0; /*We can execute sp_validatelogins*/ + END TRY + BEGIN CATCH + SET @SkipValidateLogins = 1; /*We have don't have execute rights or sp_validatelogins throws an error so skip it*/ + END CATCH; + END; /*Need execute on sp_validatelogins*/ + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM fn_my_permissions(N'[master].[dbo].[sp_MSgetalertinfo]', N'OBJECT') AS fmp + WHERE fmp.permission_name = N'EXECUTE' + ) + BEGIN + SET @SkipGetAlertInfo = 1; + END; /*Need execute on sp_MSgetalertinfo*/ + + IF ISNULL(@SkipModel, 0) != 1 /*If @SkipModel hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'model' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM model.sys.objects + ) + BEGIN + SET @SkipModel = 0; /*We have read permissions in the model database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipModel = 1; /*We have read permissions in the model database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipModel = 1; /*We don't have read permissions in the model database*/ + END; + END; + + IF ISNULL(@SkipMSDB_objs, 0) != 1 /*If @SkipMSDB_objs hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'msdb' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM msdb.sys.objects + ) + BEGIN + SET @SkipMSDB_objs = 0; /*We have read permissions in the msdb database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipMSDB_objs = 1; /*We have read permissions in the msdb database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipMSDB_objs = 1; /*We don't have read permissions in the msdb database*/ + END; + END; + IF ISNULL(@SkipMSDB_jobs, 0) != 1 /*If @SkipMSDB_jobs hasn't been set to 1 by the caller*/ + BEGIN + IF EXISTS + ( + SELECT 1/0 + FROM @db_perms + WHERE database_name = N'msdb' + ) + BEGIN + BEGIN TRY + IF EXISTS + ( + SELECT 1/0 + FROM msdb.dbo.sysjobs + ) + BEGIN + SET @SkipMSDB_jobs = 0; /*We have read permissions in the msdb database, and can view the objects*/ + END; + END TRY + BEGIN CATCH + SET @SkipMSDB_jobs = 1; /*We have read permissions in the msdb database ... oh wait we got tricked, we can't view the objects*/ + END CATCH; + END; + ELSE + BEGIN + SET @SkipMSDB_jobs = 1; /*We don't have read permissions in the msdb database*/ + END; + END; + END; SET @crlf = NCHAR(13) + NCHAR(10); SET @ResultText = 'sp_Blitz Results: ' + @crlf; + + /* Last startup */ + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC(23, 2)) + FROM sys.databases + WHERE database_id = 2; + IF @DaysUptime = 0 + SET @DaysUptime = .01; + + /* + Set the session state of Numeric_RoundAbort to off if any databases have Numeric Round-Abort enabled. + Stops arithmetic overflow errors during data conversion. See Github issue #2302 for more info. + */ + IF ( (8192 & @@OPTIONS) = 8192 ) /* Numeric RoundAbort is currently on, so we may need to turn it off temporarily */ + BEGIN + IF EXISTS (SELECT 1 + FROM sys.databases + WHERE is_numeric_roundabort_on = 1) /* A database has it turned on */ + BEGIN + SET @NeedToTurnNumericRoundabortBackOn = 1; + SET NUMERIC_ROUNDABORT OFF; + END; + END; + + + + /* --TOURSTOP01-- See https://www.BrentOzar.com/go/blitztour for a guided tour. @@ -191,6 +501,47 @@ AS Finding NVARCHAR(128) ); + /* First Responder Kit consistency (temporary tables) */ + + IF(OBJECT_ID('tempdb..#FRKObjects') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #FRKObjects;'; + END; + + -- this one represents FRK objects + CREATE TABLE #FRKObjects ( + DatabaseName VARCHAR(256) NOT NULL, + ObjectSchemaName VARCHAR(256) NULL, + ObjectName VARCHAR(256) NOT NULL, + ObjectType VARCHAR(256) NOT NULL, + MandatoryComponent BIT NOT NULL + ); + + + IF(OBJECT_ID('tempdb..#StatementsToRun4FRKVersionCheck') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #StatementsToRun4FRKVersionCheck;'; + END; + + + -- This one will contain the statements to be executed + -- order: 1- Mandatory, 2- VersionCheckMode, 3- VersionCheck + + CREATE TABLE #StatementsToRun4FRKVersionCheck ( + StatementId INT IDENTITY(1,1), + CheckName VARCHAR(256), + SubjectName VARCHAR(256), + SubjectFullPath VARCHAR(1024), + StatementText NVARCHAR(MAX), + StatementOutputsCounter BIT, + OutputCounterExpectedValue INT, + StatementOutputsExecRet BIT, + StatementOutputsDateTime BIT + ); + + /* End of First Responder Kit consistency (temporary tables) */ + + /* You can build your own table with a list of checks to skip. For example, you might have some databases that you don't care about, or some checks you don't @@ -202,10 +553,6 @@ AS want to skip. This part of the code checks those parameters, gets the list, and then saves those in a temp table. As we run each check, we'll see if we need to skip it. - - Really anal-retentive users will note that the @SkipChecksServer parameter is - not used. YET. We added that parameter in so that we could avoid changing the - stored proc's surface area (interface) later. */ /* --TOURSTOP07-- */ IF OBJECT_ID('tempdb..#SkipChecks') IS NOT NULL @@ -218,6 +565,137 @@ AS ); CREATE CLUSTERED INDEX IX_CheckID_DatabaseName ON #SkipChecks(CheckID, DatabaseName); + INSERT INTO #SkipChecks + (DatabaseName) + SELECT + DB_NAME(d.database_id) + FROM sys.databases AS d + WHERE LOWER(d.name) IN ('dbatools', 'dbadmin', 'dbmaintenance', 'rdsadmin') + OPTION(RECOMPILE); + + /*Skip checks for database where we don't have read permissions*/ + INSERT INTO + #SkipChecks + ( + DatabaseName + ) + SELECT + DB_NAME(d.database_id) + FROM sys.databases AS d + WHERE NOT EXISTS + ( + SELECT + 1/0 + FROM @db_perms AS dp + WHERE dp.database_name = DB_NAME(d.database_id) + ); + + /*Skip individial checks where we don't have permissions*/ + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 29, NULL)) AS v (DatabaseName, CheckID, ServerName) /*Looks for user tables in model*/ + WHERE @SkipModel = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 28, NULL)) AS v (DatabaseName, CheckID, ServerName) /*Tables in the MSDB Database*/ + WHERE @SkipMSDB_objs = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES + /*sysjobs checks*/ + (NULL, 6, NULL), /*Jobs Owned By Users*/ + (NULL, 57, NULL), /*SQL Agent Job Runs at Startup*/ + (NULL, 79, NULL), /*Shrink Database Job*/ + (NULL, 94, NULL), /*Agent Jobs Without Failure Emails*/ + (NULL, 123, NULL), /*Agent Jobs Starting Simultaneously*/ + (NULL, 180, NULL), /*Shrink Database Step In Maintenance Plan*/ + (NULL, 181, NULL), /*Repetitive Maintenance Tasks*/ + + /*sysalerts checks*/ + (NULL, 30, NULL), /*Not All Alerts Configured*/ + (NULL, 59, NULL), /*Alerts Configured without Follow Up*/ + (NULL, 61, NULL), /*No Alerts for Sev 19-25*/ + (NULL, 96, NULL), /*No Alerts for Corruption*/ + (NULL, 98, NULL), /*Alerts Disabled*/ + (NULL, 219, NULL), /*Alerts Without Event Descriptions*/ + + /*sysoperators*/ + (NULL, 31, NULL) /*No Operators Configured/Enabled*/ + ) AS v (DatabaseName, CheckID, ServerName) + WHERE @SkipMSDB_jobs = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 68, NULL)) AS v (DatabaseName, CheckID, ServerName) /*DBCC command*/ + WHERE @sa = 0; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 69, NULL)) AS v (DatabaseName, CheckID, ServerName) /*DBCC command*/ + WHERE @sa = 0; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 92, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_fixeddrives*/ + WHERE @SkipXPFixedDrives = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 106, NULL)) AS v (DatabaseName, CheckID, ServerName) /*alter trace*/ + WHERE @SkipTrace = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 211, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_regread*/ + WHERE @sa = 0; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 212, NULL)) AS v (DatabaseName, CheckID, ServerName) /*xp_regread*/ + WHERE @SkipXPCMDShell = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 2301, NULL)) AS v (DatabaseName, CheckID, ServerName) /*sp_validatelogins*/ + WHERE @SkipValidateLogins = 1; + + INSERT #SkipChecks (DatabaseName, CheckID, ServerName) + SELECT + v.* + FROM (VALUES(NULL, 73, NULL)) AS v (DatabaseName, CheckID, ServerName) /*sp_validatelogins*/ + WHERE @SkipGetAlertInfo = 1; + + IF @sa = 0 + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + '' AS URL , + 'User ''' + @SUSER_NAME + ''' is not part of the sysadmin role, so we skipped some checks that are not possible due to lack of permissions.' AS Details; + END; + /*End of SkipsChecks added due to permissions*/ + IF @SkipChecksTable IS NOT NULL AND @SkipChecksSchema IS NOT NULL AND @SkipChecksDatabase IS NOT NULL @@ -225,35 +703,42 @@ AS IF @Debug IN (1, 2) RAISERROR('Inserting SkipChecks', 0, 1) WITH NOWAIT; - SET @StringToExecute = 'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) + SET @StringToExecute = N'INSERT INTO #SkipChecks(DatabaseName, CheckID, ServerName ) SELECT DISTINCT DatabaseName, CheckID, ServerName - FROM ' + QUOTENAME(@SkipChecksDatabase) + '.' + QUOTENAME(@SkipChecksSchema) + '.' + QUOTENAME(@SkipChecksTable) - + ' WHERE ServerName IS NULL OR ServerName = SERVERPROPERTY(''ServerName'') OPTION (RECOMPILE);'; + FROM ' + IF LTRIM(RTRIM(@SkipChecksServer)) <> '' + BEGIN + SET @StringToExecute += QUOTENAME(@SkipChecksServer) + N'.'; + END + SET @StringToExecute += QUOTENAME(@SkipChecksDatabase) + N'.' + QUOTENAME(@SkipChecksSchema) + N'.' + QUOTENAME(@SkipChecksTable) + + N' WHERE ServerName IS NULL OR ServerName = CAST(SERVERPROPERTY(''ServerName'') AS NVARCHAR(128)) OPTION (RECOMPILE);'; EXEC(@StringToExecute); END; - IF NOT EXISTS ( SELECT 1 + -- Flag for Windows OS to help with Linux support + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_os_host_info' ) + BEGIN + SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; + END; + ELSE + BEGIN + SELECT @IsWindowsOperatingSystem = 1 ; + END; + + + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 106 ) AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 BEGIN - -- Flag for Windows OS to help with Linux support - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_os_host_info' ) - BEGIN - SELECT @IsWindowsOperatingSystem = CASE WHEN host_platform = 'Windows' THEN 1 ELSE 0 END FROM sys.dm_os_host_info ; - END; - ELSE - BEGIN - SELECT @IsWindowsOperatingSystem = 1 ; - END; select @curr_tracefilename = [path] from sys.traces where is_default = 1 ; set @curr_tracefilename = reverse(@curr_tracefilename); -- Set the trace file path separator based on underlying OS - IF (@IsWindowsOperatingSystem = 1) + IF (@IsWindowsOperatingSystem = 1) AND @curr_tracefilename IS NOT NULL BEGIN select @indx = patindex('%\%', @curr_tracefilename) ; set @curr_tracefilename = reverse(@curr_tracefilename) ; @@ -292,35 +777,59 @@ AS 'Since you have databases with compatibility_level < 90, we can''t run @CheckUserDatabaseObjects = 1. To find them: SELECT * FROM sys.databases WHERE compatibility_level < 90' AS Details; END; - /* --TOURSTOP08-- */ /* If the server is Amazon RDS, skip checks that it doesn't allow */ IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' - AND LEFT(CAST(SERVERPROPERTY('ServerName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND db_id('rdsadmin') IS NOT NULL + AND EXISTS(SELECT * FROM master.sys.all_objects WHERE name IN ('rds_startup_tasks', 'rds_help_revlogin', 'rds_hexadecimal', 'rds_failover_tracking', 'rds_database_tracking', 'rds_track_change')) BEGIN - INSERT INTO #SkipChecks (CheckID) VALUES (6); - INSERT INTO #SkipChecks (CheckID) VALUES (29); - INSERT INTO #SkipChecks (CheckID) VALUES (30); - INSERT INTO #SkipChecks (CheckID) VALUES (31); - INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file */ - INSERT INTO #SkipChecks (CheckID) VALUES (57); - INSERT INTO #SkipChecks (CheckID) VALUES (59); - INSERT INTO #SkipChecks (CheckID) VALUES (61); - INSERT INTO #SkipChecks (CheckID) VALUES (62); - INSERT INTO #SkipChecks (CheckID) VALUES (68); - INSERT INTO #SkipChecks (CheckID) VALUES (69); - INSERT INTO #SkipChecks (CheckID) VALUES (73); - INSERT INTO #SkipChecks (CheckID) VALUES (79); - INSERT INTO #SkipChecks (CheckID) VALUES (92); - INSERT INTO #SkipChecks (CheckID) VALUES (94); - INSERT INTO #SkipChecks (CheckID) VALUES (96); - INSERT INTO #SkipChecks (CheckID) VALUES (98); + 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 (29); /* tables in model database created by users - not allowed */ + INSERT INTO #SkipChecks (CheckID) VALUES (40); /* TempDB only has one data file in RDS */ + INSERT INTO #SkipChecks (CheckID) VALUES (62); /* Database compatibility level - cannot change in RDS */ + INSERT INTO #SkipChecks (CheckID) VALUES (68); /*Check for the last good DBCC CHECKDB date - can't run DBCC DBINFO() */ + INSERT INTO #SkipChecks (CheckID) VALUES (69); /* High VLF check - requires DBCC LOGINFO permission */ + INSERT INTO #SkipChecks (CheckID) VALUES (73); /* No Failsafe Operator Configured check */ + INSERT INTO #SkipChecks (CheckID) VALUES (92); /* Drive info check - requires xp_Fixeddrives permission */ INSERT INTO #SkipChecks (CheckID) VALUES (100); /* Remote DAC disabled */ - INSERT INTO #SkipChecks (CheckID) VALUES (123); - INSERT INTO #SkipChecks (CheckID) VALUES (177); - INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans */ - INSERT INTO #SkipChecks (CheckID) VALUES (181); + INSERT INTO #SkipChecks (CheckID) VALUES (177); /* Disabled Internal Monitoring Features check - requires dm_server_registry access */ + INSERT INTO #SkipChecks (CheckID) VALUES (180); /* 180/181 are maintenance plans checks - Maint plans not available in RDS*/ + INSERT INTO #SkipChecks (CheckID) VALUES (181); /*Find repetitive maintenance tasks*/ + + -- can check errorlog using rdsadmin.dbo.rds_read_error_log, so allow this check + --INSERT INTO #SkipChecks (CheckID) VALUES (193); /* xp_readerrorlog checking for IFI */ + + INSERT INTO #SkipChecks (CheckID) VALUES (211); /* xp_regread not allowed - checking for power saving */ + INSERT INTO #SkipChecks (CheckID) VALUES (212); /* xp_regread not allowed - checking for additional instances */ + INSERT INTO #SkipChecks (CheckID) VALUES (2301); /* sp_validatelogins called by Invalid login defined with Windows Authentication */ + + -- Following are skipped due to limited permissions in msdb/SQLAgent in RDS + INSERT INTO #SkipChecks (CheckID) VALUES (30); /* SQL Server Agent alerts not configured */ + INSERT INTO #SkipChecks (CheckID) VALUES (31); /* check whether we have NO ENABLED operators */ + INSERT INTO #SkipChecks (CheckID) VALUES (57); /* SQL Agent Job Runs at Startup */ + INSERT INTO #SkipChecks (CheckID) VALUES (59); /* Alerts Configured without Follow Up */ + INSERT INTO #SkipChecks (CheckID) VALUES (61); /*SQL Server Agent alerts do not exist for severity levels 19 through 25*/ + INSERT INTO #SkipChecks (CheckID) VALUES (79); /* Shrink Database Job check */ + INSERT INTO #SkipChecks (CheckID) VALUES (94); /* job failure without operator notification check */ + INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ + INSERT INTO #SkipChecks (CheckID) VALUES (98); /* check for disabled alerts */ + INSERT INTO #SkipChecks (CheckID) VALUES (123); /* Agent Jobs Starting Simultaneously */ + INSERT INTO #SkipChecks (CheckID) VALUES (219); /* check for alerts that do NOT include event descriptions in their outputs via email/pager/net-send */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://aws.amazon.com/rds/sqlserver/' AS URL , + 'Amazon RDS detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; END; /* Amazon RDS skipped checks */ /* If the server is ExpressEdition, skip checks that it doesn't allow */ @@ -331,8 +840,68 @@ AS INSERT INTO #SkipChecks (CheckID) VALUES (61); /* Agent alerts 19-25 */ INSERT INTO #SkipChecks (CheckID) VALUES (73); /* Failsafe operator */ INSERT INTO #SkipChecks (CheckID) VALUES (96); /* Agent alerts for corruption */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://stackoverflow.com/questions/1169634/limitations-of-sql-server-express' AS URL , + 'Express Edition detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; END; /* Express Edition skipped checks */ + /* If the server is an Azure Managed Instance, skip checks that it doesn't allow */ + IF SERVERPROPERTY('EngineEdition') = 8 + BEGIN + INSERT INTO #SkipChecks (CheckID) VALUES (1); /* Full backups - because of the MI GUID name bug mentioned here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ + INSERT INTO #SkipChecks (CheckID) VALUES (2); /* Log backups - because of the MI GUID name bug mentioned here: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1481 */ + 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 */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'model'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'msdb'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID, DatabaseName) VALUES (80, 'tempdb'); /* Max file size set */ + INSERT INTO #SkipChecks (CheckID) VALUES (224); /* CheckID 224 - Performance - SSRS/SSAS/SSIS Installed */ + INSERT INTO #SkipChecks (CheckID) VALUES (92); /* CheckID 92 - drive space */ + INSERT INTO #SkipChecks (CheckID) VALUES (258);/* CheckID 258 - Security - SQL Server service is running as LocalSystem or NT AUTHORITY\SYSTEM */ + INSERT INTO #SkipChecks (CheckID) VALUES (259);/* CheckID 259 - Security - SQL Server Agent service is running as LocalSystem or NT AUTHORITY\SYSTEM */ + INSERT INTO #SkipChecks (CheckID) VALUES (260); /* CheckID 260 - Security - SQL Server service account is member of Administrators */ + INSERT INTO #SkipChecks (CheckID) VALUES (261); /*CheckID 261 - Security - SQL Server Agent service account is member of Administrators */ + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 223 AS CheckID , + 0 AS Priority , + 'Informational' AS FindingsGroup , + 'Some Checks Skipped' AS Finding , + 'https://docs.microsoft.com/en-us/azure/sql-database/sql-database-managed-instance-index' AS URL , + 'Managed Instance detected, so we skipped some checks that are not currently possible, relevant, or practical there.' AS Details; + END; /* Azure Managed Instance skipped checks */ /* That's the end of the SkipChecks stuff. @@ -347,11 +916,11 @@ AS CheckID INT ); - IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL - DROP TABLE #Recompile; - CREATE TABLE #Recompile( - DBName varchar(200), - ProcName varchar(300), + IF OBJECT_ID ('tempdb..#Recompile') IS NOT NULL + DROP TABLE #Recompile; + CREATE TABLE #Recompile( + DBName varchar(200), + ProcName varchar(300), RecompileFlag varchar(1), SPSchema varchar(50) ); @@ -374,8 +943,6 @@ AS CREATE TABLE #DatabaseScopedConfigurationDefaults (ID INT IDENTITY(1,1), configuration_id INT, [name] NVARCHAR(60), default_value sql_variant, default_value_for_secondary sql_variant, CheckID INT, ); - - IF OBJECT_ID('tempdb..#DBCCs') IS NOT NULL DROP TABLE #DBCCs; CREATE TABLE #DBCCs @@ -389,7 +956,6 @@ AS DbName NVARCHAR(128) NULL ); - IF OBJECT_ID('tempdb..#LogInfo2012') IS NOT NULL DROP TABLE #LogInfo2012; CREATE TABLE #LogInfo2012 @@ -440,11 +1006,13 @@ AS DROP TABLE #driveInfo; CREATE TABLE #driveInfo ( - drive NVARCHAR , - SIZE DECIMAL(18, 2) + drive NVARCHAR(2), + logical_volume_name NVARCHAR(36), --Limit is 32 for NTFS, 11 for FAT + available_MB DECIMAL(18, 0), + total_MB DECIMAL(18, 0), + used_percent DECIMAL(18, 2) ); - IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL DROP TABLE #dm_exec_query_stats; CREATE TABLE #dm_exec_query_stats @@ -499,7 +1067,7 @@ AS ( LogDate DATETIME , ProcessInfo NVARCHAR(20) , - [Text] NVARCHAR(1000) + [Text] NVARCHAR(1000) ); IF OBJECT_ID('tempdb..#fnTraceGettable') IS NOT NULL @@ -521,6 +1089,15 @@ AS DBUserName NVARCHAR(256) ); + IF OBJECT_ID('tempdb..#Instances') IS NOT NULL + DROP TABLE #Instances; + CREATE TABLE #Instances + ( + Instance_Number NVARCHAR(MAX) , + Instance_Name NVARCHAR(MAX) , + Data_Field NVARCHAR(MAX) + ); + IF OBJECT_ID('tempdb..#IgnorableWaits') IS NOT NULL DROP TABLE #IgnorableWaits; CREATE TABLE #IgnorableWaits (wait_type NVARCHAR(60)); @@ -543,6 +1120,7 @@ AS INSERT INTO #IgnorableWaits VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT'); INSERT INTO #IgnorableWaits VALUES ('FT_IFTSHC_MUTEX'); INSERT INTO #IgnorableWaits VALUES ('HADR_CLUSAPI_CALL'); + INSERT INTO #IgnorableWaits VALUES ('HADR_FABRIC_CALLBACK'); INSERT INTO #IgnorableWaits VALUES ('HADR_FILESTREAM_IOMGR_IOCOMPLETION'); INSERT INTO #IgnorableWaits VALUES ('HADR_LOGCAPTURE_WAIT'); INSERT INTO #IgnorableWaits VALUES ('HADR_NOTIFICATION_DEQUEUE'); @@ -551,8 +1129,17 @@ AS INSERT INTO #IgnorableWaits VALUES ('LAZYWRITER_SLEEP'); INSERT INTO #IgnorableWaits VALUES ('LOGMGR_QUEUE'); INSERT INTO #IgnorableWaits VALUES ('ONDEMAND_TASK_QUEUE'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_DRAIN_WORKER'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_LOG_CACHE'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_TRAN_LIST'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_WORKER_SYNC'); + INSERT INTO #IgnorableWaits VALUES ('PARALLEL_REDO_WORKER_WAIT_WORK'); + INSERT INTO #IgnorableWaits VALUES ('POPULATE_LOCK_ORDINALS'); INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_HADR_LEASE_MECHANISM'); + INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_OS_FLUSHFILEBUFFERS'); INSERT INTO #IgnorableWaits VALUES ('PREEMPTIVE_SP_SERVER_DIAGNOSTICS'); + INSERT INTO #IgnorableWaits VALUES ('PVS_PREALLOCATE'); + INSERT INTO #IgnorableWaits VALUES ('PWAIT_EXTENSIBILITY_CLEANUP_TASK'); INSERT INTO #IgnorableWaits VALUES ('QDS_ASYNC_QUEUE'); INSERT INTO #IgnorableWaits VALUES ('QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP'); INSERT INTO #IgnorableWaits VALUES ('QDS_PERSIST_TASK_MAIN_LOOP_SLEEP'); @@ -561,16 +1148,18 @@ AS INSERT INTO #IgnorableWaits VALUES ('REQUEST_FOR_DEADLOCK_SEARCH'); INSERT INTO #IgnorableWaits VALUES ('SLEEP_SYSTEMTASK'); INSERT INTO #IgnorableWaits VALUES ('SLEEP_TASK'); + INSERT INTO #IgnorableWaits VALUES ('SOS_WORK_DISPATCHER'); INSERT INTO #IgnorableWaits VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP'); INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_BUFFER_FLUSH'); INSERT INTO #IgnorableWaits VALUES ('SQLTRACE_INCREMENTAL_FLUSH_SLEEP'); INSERT INTO #IgnorableWaits VALUES ('UCS_SESSION_REGISTRATION'); + INSERT INTO #IgnorableWaits VALUES ('VDI_CLIENT_OTHER'); INSERT INTO #IgnorableWaits VALUES ('WAIT_XTP_OFFLINE_CKPT_NEW_LOG'); INSERT INTO #IgnorableWaits VALUES ('WAITFOR'); INSERT INTO #IgnorableWaits VALUES ('XE_DISPATCHER_WAIT'); INSERT INTO #IgnorableWaits VALUES ('XE_LIVE_TARGET_TVF'); INSERT INTO #IgnorableWaits VALUES ('XE_TIMER_EVENT'); - + IF @Debug IN (1, 2) RAISERROR('Setting @MsSinceWaitsCleared', 0, 1) WITH NOWAIT; SELECT @MsSinceWaitsCleared = DATEDIFF(MINUTE, create_date, CURRENT_TIMESTAMP) * 60000.0 @@ -597,8 +1186,10 @@ AS 240, 'Wait Stats', 'Wait Stats Have Been Cleared', - 'https://BrentOzar.com/go/waits', - 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + CONVERT(NVARCHAR(100), DATEADD(ms, (-1 * @MsSinceWaitsCleared), GETDATE()), 120)); + 'https://www.brentozar.com/go/waits', + 'Someone ran DBCC SQLPERF to clear sys.dm_os_wait_stats at approximately: ' + + CONVERT(NVARCHAR(100), + DATEADD(MINUTE, (-1. * (@MsSinceWaitsCleared) / 1000. / 60.), GETDATE()), 120)); END; /* @CpuMsSinceWaitsCleared is used for waits stats calculations */ @@ -608,7 +1199,6 @@ AS SELECT @CpuMsSinceWaitsCleared = @MsSinceWaitsCleared * scheduler_count FROM sys.dm_os_sys_info; - /* If we're outputting CSV or Markdown, don't bother checking the plan cache because we cannot export plans. */ IF @OutputType = 'CSV' OR @OutputType = 'MARKDOWN' SET @CheckProcedureCache = 0; @@ -617,12 +1207,11 @@ AS IF @OutputType = 'MARKDOWN' SET @CheckServerInfo = 1; - /* Only run CheckUserDatabaseObjects if there are less than 50 databases. */ IF @BringThePain = 0 AND 50 <= (SELECT COUNT(*) FROM sys.databases) AND @CheckUserDatabaseObjects = 1 BEGIN SET @CheckUserDatabaseObjects = 0; - PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary insanity for the server and/or user.'; + PRINT 'Running sp_Blitz @CheckUserDatabaseObjects = 1 on a server with 50+ databases may cause temporary problems for the server and/or user.'; PRINT 'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.'; INSERT INTO #BlitzResults ( CheckID , @@ -669,6 +1258,40 @@ AS ) BEGIN + /* + Extract DBCC DBINFO data from the server. This data is used for check 2 using + the dbi_LastLogBackupTime field and check 68 using the dbi_LastKnownGood field. + NB: DBCC DBINFO is not available on AWS RDS databases so if the server is RDS + (which will have previously triggered inserting a checkID 223 record) and at + least one of the relevant checks is not being skipped then we can extract the + dbinfo information. + */ + IF NOT EXISTS + ( + SELECT 1/0 + FROM #BlitzResults + WHERE CheckID = 223 AND URL = 'https://aws.amazon.com/rds/sqlserver/' + ) AND NOT EXISTS + ( + SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID IN (2, 68) + ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Extracting DBCC DBINFO data (used in checks 2 and 68).', 0, 1, 68) WITH NOWAIT; + + EXEC sp_MSforeachdb N'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT #DBCCs + (ParentObject, + Object, + Field, + Value) + EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); + UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; + END + /* Our very first check! We'll put more comments in this one just to explain exactly how it works. First, we check to see if we're @@ -690,42 +1313,87 @@ AS IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 1) WITH NOWAIT; - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 1 AS CheckID , - d.[name] AS DatabaseName , - 1 AS Priority , - 'Backup' AS FindingsGroup , - 'Backups Not Performed Recently' AS Finding , - 'https://BrentOzar.com/go/nobak' AS URL , - 'Last backed up: ' - + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details - FROM master.sys.databases d - LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS - AND b.type = 'D' - AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ - WHERE d.database_id <> 2 /* Bonus points if you know what that means */ - AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ - AND d.is_in_standby = 0 /* Not a log shipping target database */ - AND d.source_database_id IS NULL /* Excludes database snapshots */ - AND d.name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 1) - /* - The above NOT IN filters out the databases we're not supposed to check. - */ - GROUP BY d.name - HAVING MAX(b.backup_finish_date) <= DATEADD(dd, - -7, GETDATE()) - OR MAX(b.backup_finish_date) IS NULL; + IF SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances need a special query */ + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 1 AS CheckID , + d.[name] AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backups Not Performed Recently' AS Finding , + 'https://www.brentozar.com/go/nobak' AS URL , + 'Last backed up: ' + + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details + FROM master.sys.databases d + LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'D' + AND b.server_name = SERVERPROPERTY('ServerName') /*Backupset ran on current server */ + WHERE d.database_id <> 2 /* Bonus points if you know what that means */ + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 1) + /* + The above NOT IN filters out the databases we're not supposed to check. + */ + GROUP BY d.name + HAVING MAX(b.backup_finish_date) <= DATEADD(dd, + -7, GETDATE()) + OR MAX(b.backup_finish_date) IS NULL; + END; + + ELSE /* SERVERPROPERTY('EngineName') must be 8, Azure Managed Instances */ + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 1 AS CheckID , + d.[name] AS DatabaseName , + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Backups Not Performed Recently' AS Finding , + 'https://www.brentozar.com/go/nobak' AS URL , + 'Last backed up: ' + + COALESCE(CAST(MAX(b.backup_finish_date) AS VARCHAR(25)),'never') AS Details + FROM master.sys.databases d + LEFT OUTER JOIN msdb.dbo.backupset b ON d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'D' + WHERE d.database_id <> 2 /* Bonus points if you know what that means */ + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + AND d.name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 1) + /* + The above NOT IN filters out the databases we're not supposed to check. + */ + GROUP BY d.name + HAVING MAX(b.backup_finish_date) <= DATEADD(dd, + -7, GETDATE()) + OR MAX(b.backup_finish_date) IS NULL; + END; + + + /* And there you have it. The rest of this stored procedure works the same way: it asks: @@ -766,9 +1434,10 @@ AS 1 AS Priority , 'Backup' AS FindingsGroup , 'Full Recovery Model w/o Log Backups' AS Finding , - 'https://BrentOzar.com/go/biglogs' AS URL , + 'https://www.brentozar.com/go/biglogs' AS URL , ( 'The ' + CAST(CAST((SELECT ((SUM([mf].[size]) * 8.) / 1024.) FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = d.[database_id] AND [mf].[type_desc] = 'LOG') AS DECIMAL(18,2)) AS VARCHAR(30)) + 'MB log file has not been backed up in the last week.' ) AS Details FROM master.sys.databases d + LEFT JOIN #DBCCs ll On ll.DbName = d.name And ll.Field = 'dbi_LastLogBackupTime' WHERE d.recovery_model IN ( 1, 2 ) AND d.database_id NOT IN ( 2, 3 ) AND d.source_database_id IS NULL @@ -779,15 +1448,83 @@ AS DatabaseName FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 2) - AND NOT EXISTS ( SELECT * - FROM msdb.dbo.backupset b + AND ( + ( + /* We couldn't get a value from the DBCC DBINFO data so let's check the msdb backup history information */ + [ll].[Value] Is Null + AND NOT EXISTS ( SELECT * + FROM msdb.dbo.backupset b + WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd,-7, GETDATE()) + ) + ) + OR + ( + Convert(datetime,ll.Value,21) < DATEADD(dd,-7, GETDATE()) + ) + + ); + END; + + /* + CheckID #256 is searching for backups to NUL. + */ + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 256 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT + 256 AS CheckID , + d.name AS DatabaseName, + 1 AS Priority , + 'Backup' AS FindingsGroup , + 'Log Backups to NUL' AS Finding , + 'https://www.brentozar.com/go/nul' AS URL , + N'The transaction log file has been backed up ' + CAST((SELECT count(*) + FROM msdb.dbo.backupset AS b INNER JOIN + msdb.dbo.backupmediafamily AS bmf + ON b.media_set_id = bmf.media_set_id + WHERE b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS = d.name COLLATE SQL_Latin1_General_CP1_CI_AS + AND bmf.physical_device_name = 'NUL' + AND b.type = 'L' + AND b.backup_finish_date >= DATEADD(dd, + -7, GETDATE())) AS NVARCHAR(8)) + ' time(s) to ''NUL'' in the last week, which means the backup does not exist. This breaks point-in-time recovery.' AS Details + FROM master.sys.databases AS d + WHERE d.recovery_model IN ( 1, 2 ) + AND d.database_id NOT IN ( 2, 3 ) + AND d.source_database_id IS NULL + AND d.state NOT IN(1, 6, 10) /* Not currently offline or restoring, like log shipping databases */ + AND d.is_in_standby = 0 /* Not a log shipping target database */ + AND d.source_database_id IS NULL /* Excludes database snapshots */ + --AND d.name NOT IN ( SELECT DISTINCT + -- DatabaseName + -- FROM #SkipChecks + -- WHERE CheckID IS NULL OR CheckID = 2) + AND EXISTS ( SELECT * + FROM msdb.dbo.backupset AS b INNER JOIN + msdb.dbo.backupmediafamily AS bmf + ON b.media_set_id = bmf.media_set_id WHERE d.name COLLATE SQL_Latin1_General_CP1_CI_AS = b.database_name COLLATE SQL_Latin1_General_CP1_CI_AS + AND bmf.physical_device_name = 'NUL' AND b.type = 'L' AND b.backup_finish_date >= DATEADD(dd, - -7, GETDATE()) ); + -7, GETDATE()) ); END; - /* Next up, we've got CheckID 8. (These don't have to go in order.) This one won't work on SQL Server 2005 because it relies on a new DMV that didn't @@ -814,7 +1551,7 @@ AS 230 AS Priority, ''Security'' AS FindingsGroup, ''Server Audits Running'' AS Finding, - ''https://BrentOzar.com/go/audits'' AS URL, + ''https://www.brentozar.com/go/audits'' AS URL, (''SQL Server built-in audit functionality is being used by server audit: '' + [name]) AS Details FROM sys.dm_server_audit_status OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -827,19 +1564,16 @@ AS /* But what if you need to run a query in every individual database? Hop down to the @CheckUserDatabaseObjects section. - + And that's the basic idea! You can read through the rest of the checks if you like - some more exciting stuff happens closer to the end of the stored proc, where we start doing things like checking the plan cache, but those aren't as cleanly commented. - If you'd like to contribute your own check, use one of the check - formats shown above and email it to Help@BrentOzar.com. You don't - have to pick a CheckID or a link - we'll take care of that when we - test and publish the code. Thanks! + To contribute your own checks or fix bugs, learn more here: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/blob/main/CONTRIBUTING.md */ - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 93 ) @@ -860,7 +1594,7 @@ AS 1 AS Priority , 'Backup' AS FindingsGroup , 'Backing Up to Same Drive Where Databases Reside' AS Finding , - 'https://BrentOzar.com/go/backup' AS URL , + 'https://www.brentozar.com/go/backup' AS URL , CAST(COUNT(1) AS VARCHAR(50)) + ' backups done on drive ' + UPPER(LEFT(bmf.physical_device_name, 3)) + ' in the last two weeks, where database files also live. This represents a serious risk if that array fails.' Details @@ -870,15 +1604,18 @@ AS -14, GETDATE()) ) /* Filter out databases that were recently restored: */ LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name AND rh.restore_date > DATEADD(dd, -14, GETDATE()) - WHERE UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( + WHERE UPPER(LEFT(bmf.physical_device_name, 3)) <> 'HTT' AND + bmf.physical_device_name NOT LIKE '\\%' AND -- GitHub Issue #2141 + @IsWindowsOperatingSystem = 1 AND -- GitHub Issue #1995 + UPPER(LEFT(bmf.physical_device_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) IN ( SELECT DISTINCT UPPER(LEFT(mf.physical_name COLLATE SQL_Latin1_General_CP1_CI_AS, 3)) - FROM sys.master_files AS mf ) + FROM sys.master_files AS mf + WHERE mf.database_id <> 2 ) AND rh.destination_database_name IS NULL GROUP BY UPPER(LEFT(bmf.physical_device_name, 3)); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 119 ) @@ -895,9 +1632,9 @@ AS ''Backup'' AS FindingsGroup, ''TDE Certificate Not Backed Up Recently'' AS Finding, db_name(dek.database_id) AS DatabaseName, - ''https://BrentOzar.com/go/tde'' AS URL, + ''https://www.brentozar.com/go/tde'' AS URL, ''The certificate '' + c.name + '' is used to encrypt database '' + db_name(dek.database_id) + ''. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details - FROM sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint + FROM master.sys.certificates c INNER JOIN sys.dm_database_encryption_keys dek ON c.thumbprint = dek.encryptor_thumbprint WHERE pvt_key_last_backup_date IS NULL OR pvt_key_last_backup_date <= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -924,7 +1661,7 @@ AS 1 AS Priority, ''Backup'' AS FindingsGroup, ''Encryption Certificate Not Backed Up Recently'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, + ''https://www.brentozar.com/go/tde'' AS URL, ''The certificate '' + c.name + '' is used to encrypt database backups. Last backup date: '' + COALESCE(CAST(c.pvt_key_last_backup_date AS VARCHAR(100)), ''Never'') AS Details FROM sys.certificates c INNER JOIN msdb.dbo.backupset bs ON c.thumbprint = bs.encryptor_thumbprint @@ -936,12 +1673,11 @@ AS EXECUTE(@StringToExecute); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 3 ) BEGIN - IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) + IF DATEADD(dd, -60, GETDATE()) > (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY backup_start_date) BEGIN @@ -962,11 +1698,13 @@ AS 200 AS Priority , 'Backup' AS FindingsGroup , 'MSDB Backup History Not Purged' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , + 'https://www.brentozar.com/go/history' AS URL , ( 'Database backup history retained back to ' + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; + LEFT OUTER JOIN msdb.dbo.restorehistory rh ON bs.database_name = rh.destination_database_name + WHERE rh.destination_database_name IS NULL + ORDER BY bs.backup_start_date ASC; END; END; @@ -974,7 +1712,7 @@ AS FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 186 ) BEGIN - IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY 1) + IF DATEADD(dd, -2, GETDATE()) < (SELECT TOP 1 backup_start_date FROM msdb.dbo.backupset ORDER BY backup_start_date) BEGIN @@ -995,11 +1733,11 @@ AS 200 AS Priority , 'Backup' AS FindingsGroup , 'MSDB Backup History Purged Too Frequently' AS Finding , - 'https://BrentOzar.com/go/history' AS URL , + 'https://www.brentozar.com/go/history' AS URL , ( 'Database backup history only retained back to ' + CAST(bs.backup_start_date AS VARCHAR(20)) ) AS Details FROM msdb.dbo.backupset bs - ORDER BY backup_set_id ASC; + ORDER BY backup_start_date ASC; END; END; @@ -1028,7 +1766,7 @@ AS 200 AS Priority , 'Performance' AS FindingsGroup , 'Snapshot Backups Occurring' AS Finding , - 'https://BrentOzar.com/go/snaps' AS URL , + 'https://www.brentozar.com/go/snaps' AS URL , ( CAST(COUNT(*) AS VARCHAR(20)) + ' snapshot-looking backups have occurred in the last two weeks, indicating that IO may be freezing up.') AS Details FROM msdb.dbo.backupset bs WHERE bs.type = 'D' @@ -1037,6 +1775,35 @@ AS AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()); /* In the last 2 weeks */ END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 236 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 236) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT TOP 1 236 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingsGroup , + 'Snapshotting Too Many Databases' AS Finding , + 'https://www.brentozar.com/go/toomanysnaps' AS URL , + ( CAST(SUM(1) AS VARCHAR(20)) + ' databases snapshotted at once in the last two weeks, indicating that IO may be freezing up. Microsoft does not recommend VSS snaps for 35 or more databases.') AS Details + FROM msdb.dbo.backupset bs + WHERE bs.type = 'D' + AND bs.backup_finish_date >= DATEADD(DAY, -14, GETDATE()) /* In the last 2 weeks */ + GROUP BY bs.backup_finish_date + HAVING SUM(1) >= 35 + ORDER BY SUM(1) DESC; + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 4 ) @@ -1056,7 +1823,7 @@ AS 230 AS Priority , 'Security' AS FindingsGroup , 'Sysadmins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , + 'https://www.brentozar.com/go/sa' AS URL , ( 'Login [' + l.name + '] is a sysadmin - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details FROM master.sys.syslogins l @@ -1067,7 +1834,44 @@ AS AND l.name <> 'l_certSignSmDetach'; /* Added in SQL 2016 */ END; - IF NOT EXISTS ( SELECT 1 + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE CheckID = 2301 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 2301) WITH NOWAIT; + + /* + #InvalidLogins is filled at the start during the permissions check IF we are not sysadmin + filling it now if we are sysadmin + */ + IF @sa = 1 + BEGIN + INSERT INTO #InvalidLogins + ( + [LoginSID] + ,[LoginName] + ) + EXEC sp_validatelogins; + END; + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 2301 AS CheckID , + 230 AS Priority , + '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. Sometimes empty AD groups can show up here so check thoroughly.') AS Details + FROM #InvalidLogins + ; + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 5 ) BEGIN @@ -1086,7 +1890,7 @@ AS 230 AS Priority , 'Security' AS FindingsGroup , 'Security Admins' AS Finding , - 'https://BrentOzar.com/go/sa' AS URL , + 'https://www.brentozar.com/go/sa' AS URL , ( 'Login [' + l.name + '] is a security admin - meaning they can give themselves permission to do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' ) AS Details FROM master.sys.syslogins l @@ -1114,7 +1918,7 @@ AS 230 AS [Priority] , 'Security' AS [FindingsGroup] , 'Login Can Control Server' AS [Finding] , - 'https://BrentOzar.com/go/sa' AS [URL] , + 'https://www.brentozar.com/go/sa' AS [URL] , 'Login [' + pri.[name] + '] has the CONTROL SERVER permission - meaning they can do absolutely anything in SQL Server, including dropping databases or hiding their tracks.' AS [Details] FROM sys.server_principals AS pri @@ -1133,7 +1937,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 , @@ -1146,16 +1954,15 @@ AS 230 AS Priority , 'Security' AS FindingsGroup , 'Jobs Owned By Users' AS Finding , - 'https://BrentOzar.com/go/owners' AS URL , + 'https://www.brentozar.com/go/owners' AS URL , ( 'Job [' + j.name + '] is owned by [' + SUSER_SNAME(j.owner_sid) + '] - 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-- */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -1177,7 +1984,7 @@ AS 230 AS Priority , 'Security' AS FindingsGroup , 'Stored Procedure Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , + 'https://www.brentozar.com/go/startup' AS URL , ( 'Stored procedure [master].[' + r.SPECIFIC_SCHEMA + '].[' + r.SPECIFIC_NAME @@ -1187,7 +1994,6 @@ AS 'ExecIsStartup') = 1; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 10 ) @@ -1209,7 +2015,7 @@ AS 100 AS Priority, ''Performance'' AS FindingsGroup, ''Resource Governor Enabled'' AS Finding, - ''https://BrentOzar.com/go/rg'' AS URL, + ''https://www.brentozar.com/go/rg'' AS URL, (''Resource Governor is enabled. Queries may be throttled. Make sure you understand how the Classifier Function is configured.'') AS Details FROM sys.resource_governor_configuration WHERE is_enabled = 1 OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -1219,7 +2025,6 @@ AS END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 11 ) @@ -1240,8 +2045,10 @@ AS 100 AS Priority, ''Performance'' AS FindingsGroup, ''Server Triggers Enabled'' AS Finding, - ''https://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);'; + ''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 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.'; @@ -1250,7 +2057,6 @@ AS END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 12 ) @@ -1272,18 +2078,17 @@ AS 10 AS Priority , 'Performance' AS FindingsGroup , 'Auto-Close Enabled' AS Finding , - 'https://BrentOzar.com/go/autoclose' AS URL , + 'https://www.brentozar.com/go/autoclose' AS URL , ( 'Database [' + [name] + '] has auto-close enabled. This setting can dramatically decrease performance.' ) AS Details FROM sys.databases WHERE is_auto_close_on = 1 AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 12); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 13 ) @@ -1305,18 +2110,18 @@ AS 10 AS Priority , 'Performance' AS FindingsGroup , 'Auto-Shrink Enabled' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , + 'https://www.brentozar.com/go/autoshrink' AS URL , ( 'Database [' + [name] + '] has auto-shrink enabled. This setting can dramatically decrease performance.' ) AS Details FROM sys.databases WHERE is_auto_shrink_on = 1 + AND state <> 6 /* Offline */ AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 13); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 14 ) @@ -1339,11 +2144,12 @@ AS 50 AS Priority, ''Reliability'' AS FindingsGroup, ''Page Verification Not Optimal'' AS Finding, - ''https://BrentOzar.com/go/torn'' AS URL, + ''https://www.brentozar.com/go/torn'' AS URL, (''Database ['' + [name] + ''] has '' + [page_verify_option_desc] + '' for page verification. SQL Server may have a harder time recognizing and recovering from storage corruption. Consider using CHECKSUM instead.'') COLLATE database_default AS Details FROM sys.databases WHERE page_verify_option < 2 AND name <> ''tempdb'' + AND state NOT IN (1, 6) /* Restoring, Offline */ and name not in (select distinct DatabaseName from #SkipChecks WHERE CheckID IS NULL OR CheckID = 14) OPTION (RECOMPILE);'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -1353,7 +2159,6 @@ AS END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 15 ) @@ -1375,14 +2180,14 @@ AS 110 AS Priority , 'Performance' AS FindingsGroup , 'Auto-Create Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/acs' AS URL , + 'https://www.brentozar.com/go/acs' AS URL , ( 'Database [' + [name] + '] has auto-create-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically create more, performance may suffer.' ) AS Details FROM sys.databases WHERE is_auto_create_stats_on = 0 AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 15); END; @@ -1407,18 +2212,17 @@ AS 110 AS Priority , 'Performance' AS FindingsGroup , 'Auto-Update Stats Disabled' AS Finding , - 'https://BrentOzar.com/go/aus' AS URL , + 'https://www.brentozar.com/go/aus' AS URL , ( 'Database [' + [name] + '] has auto-update-stats disabled. SQL Server uses statistics to build better execution plans, and without the ability to automatically update them, performance may suffer.' ) AS Details FROM sys.databases WHERE is_auto_update_stats_on = 0 AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 16); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 17 ) @@ -1440,24 +2244,23 @@ AS 150 AS Priority , 'Performance' AS FindingsGroup , 'Stats Updated Asynchronously' AS Finding , - 'https://BrentOzar.com/go/asyncstats' AS URL , + 'https://www.brentozar.com/go/asyncstats' AS URL , ( 'Database [' + [name] + '] has auto-update-stats-async enabled. When SQL Server gets a query for a table with out-of-date statistics, it will run the query with the stats it has - while updating stats to make later queries better. The initial run of the query may suffer, though.' ) AS Details FROM sys.databases WHERE is_auto_update_stats_async_on = 1 AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 17); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 18 ) + WHERE DatabaseName IS NULL AND CheckID = 20 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 18) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; INSERT INTO #BlitzResults ( CheckID , @@ -1468,58 +2271,25 @@ AS URL , Details ) - SELECT 18 AS CheckID , + SELECT 20 AS CheckID , [name] AS DatabaseName , - 150 AS Priority , - 'Performance' AS FindingsGroup , - 'Forced Parameterization On' AS Finding , - 'https://BrentOzar.com/go/forced' AS URL , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Date Correlation On' AS Finding , + 'https://www.brentozar.com/go/corr' AS URL , ( 'Database [' + [name] - + '] has forced parameterization enabled. SQL Server will aggressively reuse query execution plans even if the applications do not parameterize their queries. This can be a performance booster with some programming languages, or it may use universally bad execution plans when better alternatives are available for certain parameters.' ) AS Details + + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details FROM sys.databases - WHERE is_parameterization_forced = 1 - AND name NOT IN ( SELECT DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 18); + WHERE is_date_correlation_on = 1 + AND name NOT IN ( SELECT DISTINCT + DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 20); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 20 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 20) WITH NOWAIT; - - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 20 AS CheckID , - [name] AS DatabaseName , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Date Correlation On' AS Finding , - 'https://BrentOzar.com/go/corr' AS URL , - ( 'Database [' + [name] - + '] has date correlation enabled. This is not a default setting, and it has some performance overhead. It tells SQL Server that date fields in two tables are related, and SQL Server maintains statistics showing that relation.' ) AS Details - FROM sys.databases - WHERE is_date_correlation_on = 1 - AND name NOT IN ( SELECT DISTINCT - DatabaseName - FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 20); - END; - - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 21 ) + WHERE DatabaseName IS NULL AND CheckID = 21 ) BEGIN /* --TOURSTOP04-- */ IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' @@ -1541,7 +2311,7 @@ AS 200 AS Priority, ''Informational'' AS FindingsGroup, ''Database Encrypted'' AS Finding, - ''https://BrentOzar.com/go/tde'' AS URL, + ''https://www.brentozar.com/go/tde'' AS URL, (''Database ['' + [name] + ''] has Transparent Data Encryption enabled. Make absolutely sure you have backed up the certificate and private key, or else you will not be able to restore this database.'') AS Details FROM sys.databases WHERE is_encrypted = 1 @@ -1561,177 +2331,140 @@ 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 @@ -1753,7 +2486,7 @@ AS 200 AS Priority , 'Non-Default Server Config' AS FindingsGroup , cr.name AS Finding , - 'https://BrentOzar.com/go/conf' AS URL , + 'https://www.brentozar.com/go/conf' AS URL , ( 'This sp_configure option has been changed. Its default value is ' + COALESCE(CAST(cd.[DefaultValue] AS VARCHAR(100)), '(unknown)') @@ -1790,12 +2523,12 @@ AS URL , Details ) - VALUES + VALUES ( 190, 200, 'Performance', 'Non-Dynamic Memory', - 'https://BrentOzar.com/go/memory', + 'https://www.brentozar.com/go/memory', 'Minimum Server Memory setting is the same as the Maximum (both set to ' + CAST(@MinServerMemory AS NVARCHAR(50)) + '). This will not allow dynamic memory. Please revise memory settings' ); END; @@ -1835,7 +2568,7 @@ AS 200 AS Priority , 'Performance' AS FindingsGroup , cr.name AS Finding , - 'https://BrentOzar.com/go/cxpacket' AS URL , + 'https://www.brentozar.com/go/cxpacket' AS URL , ( 'Set to ' + CAST(cr.value_in_use AS NVARCHAR(50)) + ', its default value. Changing this sp_configure setting may reduce CXPACKET waits.') FROM sys.configurations cr INNER JOIN #ConfigurationDefaults cd ON cd.name = cr.name @@ -1844,7 +2577,6 @@ AS OR (cr.name = 'max degree of parallelism' AND (@NUMANodes > 1 OR @Processors > 8)); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 24 ) @@ -1867,7 +2599,7 @@ AS 170 AS Priority , 'File Configuration' AS FindingsGroup , 'System Database on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , + 'https://www.brentozar.com/go/cdrive' AS URL , ( 'The ' + DB_NAME(database_id) + ' database has a file on the C drive. Putting system databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details FROM sys.master_files @@ -1876,10 +2608,10 @@ AS 'model', 'msdb' ); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 25 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 25) WITH NOWAIT; @@ -1899,7 +2631,7 @@ AS 20 AS Priority , 'File Configuration' AS FindingsGroup , 'TempDB on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , + 'https://www.brentozar.com/go/cdrive' AS URL , CASE WHEN growth > 0 THEN ( 'The tempdb database has files on the C drive. TempDB frequently grows unpredictably, putting your server at risk of running out of C drive space and crashing hard. C is also often much slower than other drives, so performance may be suffering.' ) ELSE ( 'The tempdb database has files on the C drive. TempDB is not set to Autogrow, hopefully it is big enough. C is also often much slower than other drives, so performance may be suffering.' ) @@ -1909,10 +2641,10 @@ AS AND DB_NAME(database_id) = 'tempdb'; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 26 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 26) WITH NOWAIT; @@ -1932,7 +2664,7 @@ AS 20 AS Priority , 'Reliability' AS FindingsGroup , 'User Databases on C Drive' AS Finding , - 'https://BrentOzar.com/go/cdrive' AS URL , + 'https://www.brentozar.com/go/cdrive' AS URL , ( 'The ' + DB_NAME(database_id) + ' database has a file on the C drive. Putting databases on the C drive runs the risk of crashing the server when it runs out of space.' ) AS Details FROM sys.master_files @@ -1947,7 +2679,6 @@ AS WHERE CheckID IS NULL OR CheckID = 26 ); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 27 ) @@ -1969,16 +2700,18 @@ AS 200 AS Priority , 'Informational' AS FindingsGroup , 'Tables in the Master Database' AS Finding , - 'https://BrentOzar.com/go/mastuser' AS URL , + 'https://www.brentozar.com/go/mastuser' AS URL , ( 'The ' + name + ' table in the master database was created by end users on ' + CAST(create_date AS VARCHAR(20)) + '. 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; + WHERE is_ms_shipped = 0 + 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; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 28 ) @@ -2000,7 +2733,7 @@ AS 200 AS Priority , 'Informational' AS FindingsGroup , 'Tables in the MSDB Database' AS Finding , - 'https://BrentOzar.com/go/msdbuser' AS URL , + 'https://www.brentozar.com/go/msdbuser' AS URL , ( 'The ' + name + ' table in the msdb database was created by end users on ' + CAST(create_date AS VARCHAR(20)) @@ -2009,7 +2742,6 @@ AS WHERE is_ms_shipped = 0 AND name NOT LIKE '%DTA_%'; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 29 ) @@ -2027,11 +2759,11 @@ AS Details ) SELECT 29 AS CheckID , - 'msdb' AS DatabaseName , + 'model' AS DatabaseName , 200 AS Priority , 'Informational' AS FindingsGroup , 'Tables in the Model Database' AS Finding , - 'https://BrentOzar.com/go/model' AS URL , + 'https://www.brentozar.com/go/model' AS URL , ( 'The ' + name + ' table in the model database was created by end users on ' + CAST(create_date AS VARCHAR(20)) @@ -2040,7 +2772,6 @@ AS WHERE is_ms_shipped = 0; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 30 ) @@ -2066,13 +2797,11 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'Not All Alerts Configured' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , + 'https://www.brentozar.com/go/alert' AS URL , ( 'Not all SQL Server Agent alerts have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; END; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 59 ) @@ -2099,7 +2828,7 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'Alerts Configured without Follow Up' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , + 'https://www.brentozar.com/go/alert' AS URL , ( 'SQL Server Agent alerts have been configured but they either do not notify anyone or else they do not take any action. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; END; @@ -2129,13 +2858,12 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'No Alerts for Corruption' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , + 'https://www.brentozar.com/go/alert' AS URL , ( 'SQL Server Agent alerts do not exist for errors 823, 824, and 825. These three errors can give you notification about early hardware failure. Enabling them can prevent you a lot of heartbreak.' ) AS Details; END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 61 ) @@ -2160,7 +2888,7 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'No Alerts for Sev 19-25' AS Finding , - 'https://BrentOzar.com/go/alert' AS URL , + 'https://www.brentozar.com/go/alert' AS URL , ( 'SQL Server Agent alerts do not exist for severity levels 19 through 25. These are some very severe SQL Server errors. Knowing that these are happening may let you recover from errors faster.' ) AS Details; END; @@ -2192,21 +2920,53 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'Alerts Disabled' AS Finding , - 'https://www.BrentOzar.com/go/alerts/' AS URL , + 'https://www.brentozar.com/go/alert' AS URL , ( 'The following Alert is disabled, please review and enable if desired: ' + name ) AS Details FROM msdb.dbo.sysalerts WHERE enabled = 0; - - END; - - END; + END; + END; + --check for alerts that do NOT include event descriptions in their outputs via email/pager/net-send + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 219 + ) + BEGIN; + IF @Debug IN (1, 2) + BEGIN; + RAISERROR ('Running CheckId [%d].', 0, 1, 219) WITH NOWAIT; + END; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 31 ) - BEGIN + INSERT INTO #BlitzResults ( + CheckID + ,[Priority] + ,FindingsGroup + ,Finding + ,[URL] + ,Details + ) + SELECT 219 AS CheckID + ,200 AS [Priority] + ,'Monitoring' AS FindingsGroup + ,'Alerts Without Event Descriptions' AS Finding + ,'https://www.brentozar.com/go/alert' AS [URL] + ,('The following Alert is not including detailed event descriptions in its output messages: ' + QUOTENAME([name]) + + '. You can fix it by ticking the relevant boxes in its Properties --> Options page.') AS Details + FROM msdb.dbo.sysalerts + WHERE [enabled] = 1 + AND include_event_description = 0 --bitmask: 1 = email, 2 = pager, 4 = net send + ; + END; + + --check whether we have NO ENABLED operators! + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 31 ) + BEGIN; IF NOT EXISTS ( SELECT * FROM msdb.dbo.sysoperators WHERE enabled = 1 ) @@ -2227,14 +2987,12 @@ AS 200 AS Priority , 'Monitoring' AS FindingsGroup , 'No Operators Configured/Enabled' AS Finding , - 'https://BrentOzar.com/go/op' AS URL , + 'https://www.brentozar.com/go/op' AS URL , ( 'No SQL Server Agent operators (emails) have been configured. This is a free, easy way to get notified of corruption, job failures, or major outages even before monitoring systems pick it up.' ) AS Details; END; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 34 ) @@ -2260,15 +3018,15 @@ AS 1 AS Priority , ''Corruption'' AS FindingsGroup , ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , + ''https://www.brentozar.com/go/repair'' AS URL , ( ''Database mirroring has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_db_mirroring_auto_page_repair.'' ) AS Details - FROM (SELECT rp2.database_id, rp2.modification_time - FROM sys.dm_db_mirroring_auto_page_repair rp2 + FROM (SELECT rp2.database_id, rp2.modification_time + FROM sys.dm_db_mirroring_auto_page_repair rp2 WHERE rp2.[database_id] not in ( - SELECT db2.[database_id] - FROM sys.databases as db2 + SELECT db2.[database_id] + FROM sys.databases as db2 WHERE db2.[state] = 1 - ) ) as rp + ) ) as rp INNER JOIN master.sys.databases db ON rp.database_id = db.database_id WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE);'; @@ -2304,8 +3062,8 @@ AS 1 AS Priority , ''Corruption'' AS FindingsGroup , ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , - ( ''AlwaysOn has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details + ''https://www.brentozar.com/go/repair'' AS URL , + ( ''Availability Groups has automatically repaired at least one corrupt page in the last 30 days. For more information, query the DMV sys.dm_hadr_auto_page_repair.'' ) AS Details FROM sys.dm_hadr_auto_page_repair rp INNER JOIN master.sys.databases db ON rp.database_id = db.database_id WHERE rp.modification_time >= DATEADD(dd, -30, GETDATE()) OPTION (RECOMPILE) ;'; @@ -2317,7 +3075,6 @@ AS END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 90 ) @@ -2343,7 +3100,7 @@ AS 1 AS Priority , ''Corruption'' AS FindingsGroup , ''Database Corruption Detected'' AS Finding , - ''https://BrentOzar.com/go/repair'' AS URL , + ''https://www.brentozar.com/go/repair'' AS URL , ( ''SQL Server has detected at least one corrupt page in the last 30 days. For more information, query the system table msdb.dbo.suspect_pages.'' ) AS Details FROM msdb.dbo.suspect_pages sp INNER JOIN master.sys.databases db ON sp.database_id = db.database_id @@ -2356,7 +3113,6 @@ AS END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 36 ) @@ -2378,7 +3134,7 @@ AS 'Performance' AS FindingsGroup , 'Slow Storage Reads on Drive ' + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , + 'https://www.brentozar.com/go/slow' AS URL , 'Reads are averaging longer than 200ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS fs @@ -2409,7 +3165,7 @@ AS 'Performance' AS FindingsGroup , 'Slow Storage Writes on Drive ' + UPPER(LEFT(mf.physical_name, 1)) AS Finding , - 'https://BrentOzar.com/go/slow' AS URL , + 'https://www.brentozar.com/go/slow' AS URL , 'Writes are averaging longer than 100ms for at least one database on this drive. For specific database file speeds, run the query from the information link.' AS Details FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS fs @@ -2446,13 +3202,12 @@ AS 170 , 'File Configuration' , 'TempDB Only Has 1 Data File' , - 'https://BrentOzar.com/go/tempdb' , + 'https://www.brentozar.com/go/tempdb' , 'TempDB is only configured with one data file. More data files are usually required to alleviate SGAM contention.' ); END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 183 ) @@ -2482,7 +3237,7 @@ AS 170 , 'File Configuration' , 'TempDB Unevenly Sized Data Files' , - 'https://BrentOzar.com/go/tempdb' , + 'https://www.brentozar.com/go/tempdb' , 'TempDB data files are not configured with the same size. Unevenly sized tempdb data files will result in unevenly sized workloads.' ); END; @@ -2507,7 +3262,7 @@ AS 150 AS Priority , 'Performance' AS FindingsGroup , 'Queries Forcing Order Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , + 'https://www.brentozar.com/go/hints' AS URL , CAST(occurrence AS VARCHAR(10)) + ' instances of order hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details FROM sys.dm_exec_query_optimizer_info @@ -2534,7 +3289,7 @@ AS 150 AS Priority , 'Performance' AS FindingsGroup , 'Queries Forcing Join Hints' AS Finding , - 'https://BrentOzar.com/go/hints' AS URL , + 'https://www.brentozar.com/go/hints' AS URL , CAST(occurrence AS VARCHAR(10)) + ' instances of join hinting have been recorded since restart. This means queries are bossing the SQL Server optimizer around, and if they don''t know what they''re doing, this can cause more harm than good. This can also explain why DBA tuning efforts aren''t working.' AS Details FROM sys.dm_exec_query_optimizer_info @@ -2562,11 +3317,11 @@ AS 200 AS Priority , 'Informational' AS FindingsGroup , 'Linked Server Configured' AS Finding , - 'https://BrentOzar.com/go/link' AS URL , + 'https://www.brentozar.com/go/link' AS URL , +CASE WHEN l.remote_name = 'sa' - THEN s.data_source + THEN COALESCE(s.data_source, s.provider) + ' is configured as a linked server. Check its security configuration as it is connecting with sa, because any user who queries it will get admin-level permissions.' - ELSE s.data_source + ELSE COALESCE(s.data_source, s.provider) + ' is configured as a linked server. Check its security configuration to make sure it isn''t connecting with SA or some other bone-headed administrative login, because any user who queries it might get admin-level permissions.' END AS Details FROM sys.servers s @@ -2589,7 +3344,7 @@ AS 100 AS Priority , ''Performance'' AS FindingsGroup , ''Max Memory Set Too High'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , + ''https://www.brentozar.com/go/max'' AS URL , ''SQL Server max memory is set to '' + CAST(c.value_in_use AS VARCHAR(20)) + '' megabytes, but the server only has '' @@ -2621,7 +3376,7 @@ AS 1 AS Priority , ''Performance'' AS FindingsGroup , ''Memory Dangerously Low'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , + ''https://www.brentozar.com/go/max'' AS URL , ''The server has '' + CAST(( CAST(m.total_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes of physical memory, but only '' + CAST(( CAST(m.available_physical_memory_kb AS BIGINT) / 1024 ) AS VARCHAR(20)) + '' megabytes are available. As the server runs out of memory, there is danger of swapping to disk, which will kill performance.'' AS Details FROM sys.dm_os_sys_memory m @@ -2649,7 +3404,7 @@ AS 1 AS Priority , ''Performance'' AS FindingsGroup , ''Memory Dangerously Low in NUMA Nodes'' AS Finding , - ''https://BrentOzar.com/go/max'' AS URL , + ''https://www.brentozar.com/go/max'' AS URL , ''At least one NUMA node is reporting THREAD_RESOURCES_LOW in sys.dm_os_nodes and can no longer create threads.'' AS Details FROM sys.dm_os_nodes m WHERE node_state_desc LIKE ''%THREAD_RESOURCES_LOW%'' OPTION (RECOMPILE);'; @@ -2668,30 +3423,175 @@ AS IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 53) WITH NOWAIT; + DECLARE @AOFCI AS INT, @AOAG AS INT, @HAType AS VARCHAR(10), @errmsg AS VARCHAR(200) + + SELECT @AOAG = CAST(SERVERPROPERTY('IsHadrEnabled') AS INT) + SELECT @AOFCI = CAST(SERVERPROPERTY('IsClustered') AS INT) + + IF (SELECT COUNT(DISTINCT join_state_desc) FROM sys.dm_hadr_availability_replica_cluster_states) = 2 + BEGIN --if count is 2 both JOINED_STANDALONE and JOINED_FCI is configured + SET @AOFCI = 1 + END + + SELECT @HAType = + CASE + WHEN @AOFCI = 1 AND @AOAG =1 THEN 'FCIAG' + WHEN @AOFCI = 1 AND @AOAG =0 THEN 'FCI' + WHEN @AOFCI = 0 AND @AOAG =1 THEN 'AG' + ELSE 'STANDALONE' + END + + IF (@HAType IN ('FCIAG','FCI','AG')) + BEGIN + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node' AS Finding , + 'https://BrentOzar.com/go/node' AS URL , + 'This is a node in a cluster.' AS Details + + IF @HAType = 'AG' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + CASE ar.replica_server_name WHEN dhags.primary_replica THEN 'PRIMARY' + ELSE 'SECONDARY' + END + '=' + UPPER(ar.replica_server_name) + FROM sys.availability_groups AS ag + LEFT OUTER JOIN sys.availability_replicas AS ar ON ag.group_id = ar.group_id + LEFT OUTER JOIN sys.dm_hadr_availability_group_states as dhags ON ag.group_id = dhags.group_id + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn Availability Group (AG)' As Details + END + + IF @HAType = 'FCI' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + CASE is_current_owner + WHEN 1 THEN 'PRIMARY' + ELSE 'SECONDARY' + END + '=' + UPPER(NodeName) + FROM sys.dm_os_cluster_nodes + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn Failover Cluster Instance (FCI)' As Details + END + + IF @HAType = 'FCIAG' + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT DISTINCT 53 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Cluster Node Info' AS Finding , + 'https://BrentOzar.com/go/node' AS URL, + 'The cluster nodes are: ' + STUFF((SELECT ', ' + HighAvailabilityRoleDesc + '=' + ServerName + FROM (SELECT UPPER(ar.replica_server_name) AS ServerName + ,CASE ar.replica_server_name WHEN dhags.primary_replica THEN 'PRIMARY' + ELSE 'SECONDARY' + END AS HighAvailabilityRoleDesc + FROM sys.availability_groups AS ag + LEFT OUTER JOIN sys.availability_replicas AS ar ON ag.group_id = ar.group_id + LEFT OUTER JOIN sys.dm_hadr_availability_group_states as dhags ON ag.group_id = dhags.group_id + WHERE UPPER(CAST(SERVERPROPERTY('ServerName') AS VARCHAR)) <> ar.replica_server_name + UNION ALL + SELECT UPPER(NodeName) AS ServerName + ,CASE is_current_owner + WHEN 1 THEN 'PRIMARY' + ELSE 'SECONDARY' + END AS HighAvailabilityRoleDesc + FROM sys.dm_os_cluster_nodes) AS Z + ORDER BY 1 + FOR XML PATH ('') + ), 1, 1, '') + ' - High Availability solution used is AlwaysOn FCI with AG (Failover Cluster Instance with Availability Group).'As Details + END + END + + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 55 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; + + IF @UsualDBOwner IS NULL + SET @UsualDBOwner = SUSER_SNAME(0x01); + INSERT INTO #BlitzResults ( CheckID , + DatabaseName , Priority , FindingsGroup , Finding , URL , Details ) - SELECT TOP 1 - 53 AS CheckID , - 200 AS Priority , - 'Informational' AS FindingsGroup , - 'Cluster Node' AS Finding , - 'https://BrentOzar.com/go/node' AS URL , - 'This is a node in a cluster.' AS Details - FROM sys.dm_os_cluster_nodes; + SELECT 55 AS CheckID , + [name] AS DatabaseName , + 230 AS Priority , + 'Security' AS FindingsGroup , + 'Database Owner <> ' + @UsualDBOwner AS Finding , + 'https://www.brentozar.com/go/owndb' AS URL , + ( 'Database name: ' + [name] + ' ' + + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details + FROM sys.databases + WHERE (((SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01)) AND (name IN (N'master', N'model', N'msdb', N'tempdb'))) + OR ((SUSER_SNAME(owner_sid) <> @UsualDBOwner) AND (name NOT IN (N'master', N'model', N'msdb', N'tempdb'))) + ) + AND name NOT IN ( SELECT DISTINCT DatabaseName + FROM #SkipChecks + WHERE CheckID IS NULL OR CheckID = 55); END; IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 55 ) + WHERE DatabaseName IS NULL AND CheckID = 213 ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 55) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 213) WITH NOWAIT; INSERT INTO #BlitzResults ( CheckID , @@ -2702,20 +3602,19 @@ AS URL , Details ) - SELECT 55 AS CheckID , + SELECT 213 AS CheckID , [name] AS DatabaseName , 230 AS Priority , 'Security' AS FindingsGroup , - 'Database Owner <> SA' AS Finding , - 'https://BrentOzar.com/go/owndb' AS URL , + 'Database Owner is Unknown' AS Finding , + '' AS URL , ( 'Database name: ' + [name] + ' ' - + 'Owner name: ' + SUSER_SNAME(owner_sid) ) AS Details + + 'Owner name: ' + ISNULL(SUSER_SNAME(owner_sid),'~~ UNKNOWN ~~') ) AS Details FROM sys.databases - WHERE SUSER_SNAME(owner_sid) <> SUSER_SNAME(0x01) - AND name NOT IN ( SELECT DISTINCT - DatabaseName + WHERE SUSER_SNAME(owner_sid) is NULL + AND name NOT IN ( SELECT DISTINCT DatabaseName FROM #SkipChecks - WHERE CheckID IS NULL OR CheckID = 55); + WHERE CheckID IS NULL OR CheckID = 213); END; IF NOT EXISTS ( SELECT 1 @@ -2737,7 +3636,7 @@ AS 230 AS Priority , 'Security' AS FindingsGroup , 'SQL Agent Job Runs at Startup' AS Finding , - 'https://BrentOzar.com/go/startup' AS URL , + 'https://www.brentozar.com/go/startup' AS URL , ( 'Job [' + j.name + '] runs automatically when SQL Server Agent starts up. Make sure you know exactly what this job is doing, because it could pose a security risk.' ) AS Details FROM msdb.dbo.sysschedules sched @@ -2747,8 +3646,6 @@ AS AND sched.enabled = 1; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 97 ) @@ -2768,7 +3665,7 @@ AS 100 AS Priority , 'Performance' AS FindingsGroup , 'Unusual SQL Server Edition' AS Finding , - 'https://BrentOzar.com/go/workgroup' AS URL , + 'https://www.brentozar.com/go/workgroup' AS URL , ( 'This server is using ' + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + ', which is capped at low amounts of CPU and memory.' ) AS Details @@ -2782,6 +3679,7 @@ AS IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 154 ) + AND SERVERPROPERTY('EngineEdition') <> 8 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 154) WITH NOWAIT; @@ -2798,7 +3696,7 @@ AS 10 AS Priority , 'Performance' AS FindingsGroup , '32-bit SQL Server Installed' AS Finding , - 'https://BrentOzar.com/go/32bit' AS URL , + 'https://www.brentozar.com/go/32bit' AS URL , ( 'This server uses the 32-bit x86 binaries for SQL Server instead of the 64-bit x64 binaries. The amount of memory available for query workspace and execution plans is heavily limited.' ) AS Details WHERE CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%64%'; END; @@ -2824,7 +3722,7 @@ AS 200 AS Priority , 'Performance' AS FindingsGroup , 'Old Compatibility Level' AS Finding , - 'https://BrentOzar.com/go/compatlevel' AS URL , + 'https://www.brentozar.com/go/compatlevel' AS URL , ( 'Database ' + [name] + ' is compatibility level ' + CAST(compatibility_level AS VARCHAR(20)) @@ -2832,7 +3730,7 @@ AS FROM sys.databases WHERE name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 62) AND compatibility_level <= 90; END; @@ -2844,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 , @@ -2856,15 +3762,34 @@ AS 200 AS [Priority] , 'Monitoring' AS FindingsGroup , 'Agent Jobs Without Failure Emails' AS Finding , - 'https://BrentOzar.com/go/alerts' AS URL , + '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 - INNER JOIN ( SELECT DISTINCT - [job_id] - FROM [msdb].[dbo].[sysjobschedules] - WHERE next_run_date > 0 - ) s ON j.job_id = s.job_id + 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 @@ -2872,7 +3797,6 @@ AS AND j.category_id <> 100; /* Exclude SSRS category */ END; - IF EXISTS ( SELECT 1 FROM sys.configurations WHERE name = 'remote admin connections' @@ -2893,14 +3817,13 @@ AS Details ) SELECT 100 AS CheckID , - 50 AS Priority , + 170 AS Priority , 'Reliability' AS FindingGroup , 'Remote DAC Disabled' AS Finding , - 'https://BrentOzar.com/go/dac' AS URL , + 'https://www.brentozar.com/go/dac' AS URL , 'Remote access to the Dedicated Admin Connection (DAC) is not enabled. The DAC can make remote troubleshooting much easier when SQL Server is unresponsive.'; END; - IF EXISTS ( SELECT * FROM sys.dm_os_schedulers WHERE is_online = 0 ) @@ -2923,11 +3846,10 @@ AS 50 AS Priority , 'Performance' AS FindingGroup , 'CPU Schedulers Offline' AS Finding , - 'https://BrentOzar.com/go/schedulers' AS URL , + 'https://www.brentozar.com/go/schedulers' AS URL , 'Some CPU cores are not accessible to SQL Server due to affinity masking or licensing problems.'; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 110 ) @@ -2952,7 +3874,7 @@ AS 50 AS Priority , ''Performance'' AS FindingGroup , ''Memory Nodes Offline'' AS Finding , - ''https://BrentOzar.com/go/schedulers'' AS URL , + ''https://www.brentozar.com/go/schedulers'' AS URL , ''Due to affinity masking or licensing problems, some of the memory may not be available.'' OPTION (RECOMPILE)'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -2961,7 +3883,6 @@ AS EXECUTE(@StringToExecute); END; - IF EXISTS ( SELECT * FROM sys.databases WHERE state > 1 ) @@ -2986,7 +3907,7 @@ AS 20 AS Priority , 'Reliability' AS FindingGroup , 'Unusual Database State: ' + [state_desc] AS Finding , - 'https://BrentOzar.com/go/repair' AS URL , + 'https://www.brentozar.com/go/repair' AS URL , 'This database may not be online.' FROM sys.databases WHERE state > 1; @@ -3015,14 +3936,12 @@ AS 200 AS Priority , 'Reliability' AS FindingGroup , 'Extended Stored Procedures in Master' AS Finding , - 'https://BrentOzar.com/go/clr' AS URL , + 'https://www.brentozar.com/go/clr' AS URL , 'The [' + name + '] extended stored procedure is in the master database. CLR may be in use, and the master database now needs to be part of your backup/recovery planning.' FROM master.sys.extended_procedures; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 107 ) @@ -3042,23 +3961,22 @@ AS 50 AS Priority , 'Performance' AS FindingGroup , 'Poison Wait Detected: ' + wait_type AS Finding , - 'https://BrentOzar.com/go/poison/#' + wait_type AS URL , + 'https://www.brentozar.com/go/poison/#' + wait_type AS URL , CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. This wait often indicates killer performance problems.' FROM sys.[dm_os_wait_stats] - WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') + WHERE wait_type IN('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'POOL_LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') GROUP BY wait_type HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') AND SUM([wait_time_ms]) > 60000; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 121 ) + 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, 121) WITH NOWAIT; + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 270) WITH NOWAIT; INSERT INTO #BlitzResults ( CheckID , @@ -3068,27 +3986,28 @@ AS URL , Details ) - SELECT 121 AS CheckID , - 50 AS Priority , + SELECT 270 AS CheckID , + 1 AS Priority , 'Performance' AS FindingGroup , - 'Poison Wait Detected: Serializable Locking' AS Finding , - 'https://BrentOzar.com/go/serializable' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' - FROM sys.[dm_os_wait_stats] - WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') - HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') - AND SUM([wait_time_ms]) > 60000; + '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 @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 + + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 162 ) + WHERE DatabaseName IS NULL AND CheckID = 121 ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 121) WITH NOWAIT; INSERT INTO #BlitzResults ( CheckID , @@ -3098,24 +4017,19 @@ AS URL , Details ) - SELECT 162 AS CheckID , + SELECT 121 AS CheckID , 50 AS Priority , 'Performance' AS FindingGroup , - 'Poison Wait Detected: CMEMTHREAD & NUMA' AS Finding , - 'https://BrentOzar.com/go/poison' AS URL , - CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' - FROM sys.dm_os_nodes n - INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' - WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 - AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') - GROUP BY w.wait_type + 'Poison Wait Detected: Serializable Locking' AS Finding , + 'https://www.brentozar.com/go/serializable' AS URL , + CONVERT(VARCHAR(10), (SUM([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (SUM([wait_time_ms]) / 1000), 0), 108) + ' of LCK_M_R% waits have been recorded. This wait often indicates killer performance problems.' + FROM sys.[dm_os_wait_stats] + WHERE wait_type IN ('LCK_M_RS_S', 'LCK_M_RS_U', 'LCK_M_RIn_NL','LCK_M_RIn_S', 'LCK_M_RIn_U','LCK_M_RIn_X', 'LCK_M_RX_S', 'LCK_M_RX_U','LCK_M_RX_X') HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') AND SUM([wait_time_ms]) > 60000; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 111 ) @@ -3137,7 +4051,7 @@ AS 'Reliability' AS FindingGroup , 'Possibly Broken Log Shipping' AS Finding , d.[name] , - 'https://BrentOzar.com/go/shipping' AS URL , + 'https://www.brentozar.com/go/shipping' AS URL , d.[name] + ' is in a restoring state, but has not had a backup applied in the last two days. This is a possible indication of a broken transaction log shipping setup.' FROM [master].sys.databases d INNER JOIN [master].sys.database_mirroring dm ON d.database_id = dm.database_id @@ -3151,7 +4065,6 @@ AS END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 112 ) @@ -3165,13 +4078,15 @@ AS Priority, FindingsGroup, Finding, + DatabaseName, URL, Details) SELECT 112 AS CheckID, 100 AS Priority, ''Performance'' AS FindingsGroup, ''Change Tracking Enabled'' AS Finding, - ''https://BrentOzar.com/go/tracking'' AS URL, + d.[name], + ''https://www.brentozar.com/go/tracking'' AS URL, ( d.[name] + '' has change tracking enabled. This is not a default setting, and it has some performance overhead. It keeps track of changes to rows in tables that have change tracking turned on.'' ) AS Details FROM sys.change_tracking_databases AS ctd INNER JOIN sys.databases AS d ON ctd.database_id = d.database_id OPTION (RECOMPILE)'; IF @Debug = 2 AND @StringToExecute IS NOT NULL PRINT @StringToExecute; @@ -3180,7 +4095,6 @@ AS EXECUTE(@StringToExecute); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 116 ) @@ -3201,8 +4115,8 @@ AS 200 AS Priority , ''Informational'' AS FindingGroup , ''Backup Compression Default Off'' AS Finding , - ''https://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.'' + ''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 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);'; @@ -3233,7 +4147,7 @@ AS 100 AS Priority, ''Performance'' AS FindingsGroup, ''Memory Pressure Affecting Queries'' AS Finding, - ''https://BrentOzar.com/go/grants'' AS URL, + ''https://www.brentozar.com/go/grants'' AS URL, CAST(SUM(forced_grant_count) AS NVARCHAR(100)) + '' forced grants reported in the DMV sys.dm_exec_query_resource_semaphores, indicating memory pressure has affected query runtimes.'' FROM sys.dm_exec_query_resource_semaphores WHERE [forced_grant_count] IS NOT NULL OPTION (RECOMPILE);'; @@ -3243,8 +4157,6 @@ AS EXECUTE(@StringToExecute); END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 124 ) @@ -3259,8 +4171,12 @@ AS Finding, URL, Details) - SELECT 124, 150, 'Performance', 'Deadlocks Happening Daily', 'https://BrentOzar.com/go/deadlocks', - CAST(p.cntr_value AS NVARCHAR(100)) + ' deadlocks recorded since startup. To find them, run sp_BlitzLock.' AS Details + SELECT 124, + 150, + 'Performance', + 'Deadlocks Happening Daily', + 'https://www.brentozar.com/go/deadlocks', + CAST(CAST(p.cntr_value / @DaysUptime AS BIGINT) AS NVARCHAR(100)) + ' average deadlocks per day. To find them, run sp_BlitzLock.' AS Details FROM sys.dm_os_performance_counters p INNER JOIN sys.databases d ON d.name = 'tempdb' WHERE RTRIM(p.counter_name) = 'Number of Deadlocks/sec' @@ -3269,7 +4185,6 @@ AS AND (1.0 * p.cntr_value / NULLIF(datediff(DD,create_date,CURRENT_TIMESTAMP),0)) > 10; END; - IF DATEADD(mi, -15, GETDATE()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) AND NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -3278,6 +4193,44 @@ AS IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 125) WITH NOWAIT; + DECLARE @user_perm_sql NVARCHAR(MAX) = N''; + DECLARE @user_perm_gb_out DECIMAL(38,2); + + IF @ProductVersionMajor >= 11 + + BEGIN + + SET @user_perm_sql += N' + SELECT @user_perm_gb = CASE WHEN (pages_kb / 1024. / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), (pages_kb / 1024. / 1024.)) + ELSE NULL + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'' + '; + + END + + IF @ProductVersionMajor < 11 + + BEGIN + SET @user_perm_sql += N' + SELECT @user_perm_gb = CASE WHEN ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.)) + ELSE NULL + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'' + '; + + END + + EXEC sys.sp_executesql @user_perm_sql, + N'@user_perm_gb DECIMAL(38,2) OUTPUT', + @user_perm_gb = @user_perm_gb_out OUTPUT + INSERT INTO #BlitzResults (CheckID, Priority, @@ -3285,8 +4238,12 @@ AS Finding, URL, Details) - SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://BrentOzar.com/askbrent/plan-cache-erased-recently/', - 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' + SELECT TOP 1 125, 10, 'Performance', 'Plan Cache Erased Recently', 'https://www.brentozar.com/askbrent/plan-cache-erased-recently/', + 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + + CASE WHEN @user_perm_gb_out IS NULL + THEN '. Someone ran DBCC FREEPROCCACHE, restarted SQL Server, or it is under horrific memory pressure.' + ELSE '. You also have ' + CONVERT(NVARCHAR(20), @user_perm_gb_out) + ' GB of USERSTORE_TOKENPERM, which could indicate unusual memory consumption.' + END FROM sys.dm_exec_query_stats WITH (NOLOCK) ORDER BY creation_time; END; @@ -3306,30 +4263,34 @@ AS Finding, URL, Details) - VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://BrentOzar.com/go/priorityboost/', + VALUES(126, 5, 'Reliability', 'Priority Boost Enabled', 'https://www.brentozar.com/go/priorityboost/', 'Priority Boost sounds awesome, but it can actually cause your SQL Server to crash.'); END; IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 128 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN - IF (@ProductVersionMajor = 12 AND @ProductVersionMinor < 5000) OR - (@ProductVersionMajor = 11 AND @ProductVersionMinor < 6020) OR - (@ProductVersionMajor = 10.5 AND @ProductVersionMinor < 6000) OR - (@ProductVersionMajor = 10 AND @ProductVersionMinor < 6000) OR + IF (@ProductVersionMajor = 15 AND @ProductVersionMinor < 2000) OR + (@ProductVersionMajor = 14 AND @ProductVersionMinor < 1000) OR + (@ProductVersionMajor = 13 AND @ProductVersionMinor < 6300) OR + (@ProductVersionMajor = 12 AND @ProductVersionMinor < 6024) OR + (@ProductVersionMajor = 11 /*AND @ProductVersionMinor < 7001)*/) OR + (@ProductVersionMajor = 10.5 /*AND @ProductVersionMinor < 6000*/) OR + (@ProductVersionMajor = 10 /*AND @ProductVersionMinor < 6000*/) OR (@ProductVersionMajor = 9 /*AND @ProductVersionMinor <= 5000*/) BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 128) WITH NOWAIT; INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://BrentOzar.com/go/unsupported', - 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + '.' + - CASE WHEN @ProductVersionMajor > 9 THEN - CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' - ELSE ' is no longer support by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); + VALUES(128, 20, 'Reliability', 'Unsupported Build of SQL Server', 'https://www.brentozar.com/go/unsupported', + 'Version ' + CAST(@ProductVersionMajor AS VARCHAR(100)) + + CASE WHEN @ProductVersionMajor >= 12 THEN + '.' + CAST(@ProductVersionMinor AS VARCHAR(100)) + ' is no longer supported by Microsoft. You need to apply a service pack.' + ELSE ' is no longer supported by Microsoft. You should be making plans to upgrade to a modern version of SQL Server.' END); END; END; @@ -3338,6 +4299,7 @@ AS IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 129 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 3000 AND @ProductVersionMinor <= 3436) OR (@ProductVersionMajor = 11 AND @ProductVersionMinor = 5058) OR @@ -3357,6 +4319,7 @@ AS IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 157 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN IF (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5500 AND @ProductVersionMinor <= 5512) OR (@ProductVersionMajor = 10 AND @ProductVersionMinor >= 5750 AND @ProductVersionMinor <= 5867) OR @@ -3381,8 +4344,9 @@ AS IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 189 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ BEGIN - IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') + IF (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001 AND @@VERSION LIKE '%Standard Edition%') BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 189) WITH NOWAIT; @@ -3393,6 +4357,43 @@ AS END; END; + + /* Check if SQL 2017 but not CU3 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 216 ) + AND SERVERPROPERTY('EngineEdition') <> 8 /* Azure Managed Instances */ + BEGIN + IF (@ProductVersionMajor = 14 AND @ProductVersionMinor < 3015) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 216) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES(216, 100, 'Features', 'Missing Features', 'https://support.microsoft.com/en-us/help/4041814', + 'SQL 2017 is being used but not Cumulative Update 3. We''d recommend patching to take advantage of increased analytics when running BlitzCache.'); + END; + + END; + + /* Cumulative Update Available */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 217 ) + AND SERVERPROPERTY('EngineEdition') NOT IN (5,8) /* Azure Managed Instances and Azure SQL DB*/ + AND EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'SqlServerVersions' AND TABLE_TYPE = 'BASE TABLE') + AND NOT EXISTS (SELECT * FROM #BlitzResults WHERE CheckID IN (128, 129, 157, 189, 216)) /* Other version checks */ + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 217) WITH NOWAIT; + + INSERT INTO #BlitzResults(CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT TOP 1 217, 100, 'Reliability', 'Cumulative Update Available', COALESCE(v.Url, 'https://SQLServerUpdates.com/'), + v.MinorVersionName + ' was released on ' + CAST(CONVERT(DATETIME, v.ReleaseDate, 112) AS VARCHAR(100)) + FROM dbo.SqlServerVersions v + WHERE v.MajorVersionNumber = @ProductVersionMajor + AND v.MinorVersionNumber > @ProductVersionMinor + ORDER BY v.MinorVersionNumber DESC; + END; /* Performance - High Memory Use for In-Memory OLTP (Hekaton) */ IF NOT EXISTS ( SELECT 1 @@ -3402,7 +4403,7 @@ AS FROM sys.all_objects o WHERE o.name = 'dm_db_xtp_table_memory_stats' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 145) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) @@ -3410,21 +4411,20 @@ AS 10 AS Priority, ''Performance'' AS FindingsGroup, ''High Memory Use for In-Memory OLTP (Hekaton)'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, + ''https://www.brentozar.com/go/hekaton'' AS URL, CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton). Microsoft recommends having 2X your Hekaton table space available in memory just for Hekaton, with a max of 250GB of in-memory data regardless of your server memory capacity.'' AS Details FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' WHERE c.name = ''max server memory (MB)'' GROUP BY c.value_in_use HAVING CAST(value_in_use AS DECIMAL(38,2)) * .25 < SUM(mem.pages_kb / 1024.0) OR SUM(mem.pages_kb / 1024.0) > 250000 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.'; EXECUTE(@StringToExecute); END; - /* Performance - In-Memory OLTP (Hekaton) In Use */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -3433,7 +4433,7 @@ AS FROM sys.all_objects o WHERE o.name = 'dm_db_xtp_table_memory_stats' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 146) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) @@ -3441,13 +4441,13 @@ AS 200 AS Priority, ''Performance'' AS FindingsGroup, ''In-Memory OLTP (Hekaton) In Use'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, - CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS INT) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details + ''https://www.brentozar.com/go/hekaton'' AS URL, + CAST(CAST((SUM(mem.pages_kb / 1024.0) / CAST(value_in_use AS INT) * 100) AS DECIMAL(6,1)) AS NVARCHAR(100)) + ''% of your '' + CAST(CAST((CAST(value_in_use AS DECIMAL(38,1)) / 1024) AS MONEY) AS NVARCHAR(100)) + ''GB of your max server memory is being used for in-memory OLTP tables (Hekaton).'' AS Details FROM sys.configurations c INNER JOIN sys.dm_os_memory_clerks mem ON mem.type = ''MEMORYCLERK_XTP'' WHERE c.name = ''max server memory (MB)'' GROUP BY c.value_in_use - HAVING SUM(mem.pages_kb / 1024.0) > 10 OPTION (RECOMPILE)'; - + HAVING SUM(mem.pages_kb / 1024.0) > 1000 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.'; @@ -3462,7 +4462,7 @@ AS FROM sys.all_objects o WHERE o.name = 'dm_xtp_transaction_stats' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 147) WITH NOWAIT SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) @@ -3470,28 +4470,26 @@ AS 100 AS Priority, ''In-Memory OLTP (Hekaton)'' AS FindingsGroup, ''Transaction Errors'' AS Finding, - ''https://BrentOzar.com/go/hekaton'' AS URL, + ''https://www.brentozar.com/go/hekaton'' AS URL, ''Since restart: '' + CAST(validation_failures AS NVARCHAR(100)) + '' validation failures, '' + CAST(dependencies_failed AS NVARCHAR(100)) + '' dependency failures, '' + CAST(write_conflicts AS NVARCHAR(100)) + '' write conflicts, '' + CAST(unique_constraint_violations AS NVARCHAR(100)) + '' unique constraint violations.'' AS Details FROM sys.dm_xtp_transaction_stats WHERE validation_failures <> 0 OR dependencies_failed <> 0 OR write_conflicts <> 0 OR unique_constraint_violations <> 0 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.'; EXECUTE(@StringToExecute); END; - - /* Reliability - Database Files on Network File Shares */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 148 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 148) WITH NOWAIT; INSERT INTO #BlitzResults @@ -3508,14 +4506,14 @@ AS 170 AS Priority , 'Reliability' AS FindingsGroup , 'Database Files on Network File Shares' AS Finding , - 'https://BrentOzar.com/go/nas' AS URL , + 'https://www.brentozar.com/go/nas' AS URL , ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details FROM sys.databases d INNER JOIN sys.master_files mf ON d.database_id = mf.database_id WHERE mf.physical_name LIKE '\\%' AND d.name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 148); END; @@ -3524,7 +4522,7 @@ AS FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 149 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 149) WITH NOWAIT; INSERT INTO #BlitzResults @@ -3541,69 +4539,71 @@ AS 170 AS Priority , 'Reliability' AS FindingsGroup , 'Database Files Stored in Azure' AS Finding , - 'https://BrentOzar.com/go/azurefiles' AS URL , + 'https://www.brentozar.com/go/azurefiles' AS URL , ( 'Files for this database are on: ' + LEFT(mf.physical_name, 30)) AS Details FROM sys.databases d INNER JOIN sys.master_files mf ON d.database_id = mf.database_id WHERE mf.physical_name LIKE 'http://%' AND d.name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 149); END; - /* Reliability - Errors Logged Recently in the Default Trace */ - + /* First, let's check that there aren't any issues with the trace files */ BEGIN TRY - - INSERT INTO #fnTraceGettable - ( TextData , - DatabaseName , - EventClass , - Severity , - StartTime , - EndTime , - Duration , - NTUserName , - NTDomainName , - HostName , - ApplicationName , - LoginName , - DBUserName - ) - SELECT TOP 20000 - CONVERT(NVARCHAR(4000),t.TextData) , - t.DatabaseName , - t.EventClass , - t.Severity , - t.StartTime , - t.EndTime , - t.Duration , - t.NTUserName , - t.NTDomainName , - t.HostName , - t.ApplicationName , - t.LoginName , - t.DBUserName - FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t - WHERE - ( - t.EventClass = 22 - AND t.Severity >= 17 - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - ) - OR - ( - t.EventClass IN (92, 93) - AND t.StartTime > DATEADD(dd, -30, GETDATE()) - AND t.Duration > 15000000 - ) - OR - ( - t.EventClass IN (94, 95, 116) - ) + + IF @SkipTrace = 0 + BEGIN + INSERT INTO #fnTraceGettable + ( TextData , + DatabaseName , + EventClass , + Severity , + StartTime , + EndTime , + Duration , + NTUserName , + NTDomainName , + HostName , + ApplicationName , + LoginName , + DBUserName + ) + SELECT TOP 20000 + CONVERT(NVARCHAR(4000),t.TextData) , + t.DatabaseName , + t.EventClass , + t.Severity , + t.StartTime , + t.EndTime , + t.Duration , + t.NTUserName , + t.NTDomainName , + t.HostName , + t.ApplicationName , + t.LoginName , + t.DBUserName + FROM sys.fn_trace_gettable(@base_tracefilename, DEFAULT) t + WHERE + ( + t.EventClass = 22 + AND t.Severity >= 17 + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + ) + OR + ( + t.EventClass IN (92, 93) + AND t.StartTime > DATEADD(dd, -30, GETDATE()) + AND t.Duration > 15000000 + ) + OR + ( + t.EventClass IN (94, 95, 116) + ) + END; SET @TraceFileIssue = 0 @@ -3635,7 +4635,7 @@ AS 50 AS Priority , 'Reliability' AS FindingsGroup , 'There Is An Error With The Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , + 'https://www.brentozar.com/go/defaulttrace' AS URL , 'Somebody has been messing with your trace files. Check the files are present at ' + @base_tracefilename AS Details END @@ -3659,10 +4659,10 @@ AS ) SELECT DISTINCT 150 AS CheckID , t.DatabaseName, - 50 AS Priority , + 170 AS Priority , 'Reliability' AS FindingsGroup , 'Errors Logged Recently in the Default Trace' AS Finding , - 'https://BrentOzar.com/go/defaulttrace' AS URL , + 'https://www.brentozar.com/go/defaulttrace' AS URL , CAST(t.TextData AS NVARCHAR(4000)) AS Details FROM #fnTraceGettable t WHERE t.EventClass = 22 @@ -3671,7 +4671,6 @@ AS --AND t.StartTime > DATEADD(dd, -30, GETDATE()); END; - /* Performance - File Growths Slow */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -3679,7 +4678,7 @@ AS AND @base_tracefilename IS NOT NULL AND @TraceFileIssue = 0 BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 151) WITH NOWAIT; INSERT INTO #BlitzResults @@ -3696,7 +4695,7 @@ AS 50 AS Priority , 'Performance' AS FindingsGroup , 'File Growths Slow' AS Finding , - 'https://BrentOzar.com/go/filegrowth' AS URL , + 'https://www.brentozar.com/go/filegrowth' AS URL , CAST(COUNT(*) AS NVARCHAR(100)) + ' growths took more than 15 seconds each. Consider setting file autogrowth to a smaller increment.' AS Details FROM #fnTraceGettable t WHERE t.EventClass IN (92, 93) @@ -3707,43 +4706,47 @@ AS HAVING COUNT(*) > 1; END; - /* Performance - Many Plans for One Query */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 160 ) AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'query_hash') BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 160) WITH NOWAIT; - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SET @StringToExecute = N'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) SELECT TOP 1 160 AS CheckID, 100 AS Priority, ''Performance'' AS FindingsGroup, ''Many Plans for One Query'' AS Finding, - ''https://BrentOzar.com/go/parameterization'' AS URL, + ''https://www.brentozar.com/go/parameterization'' AS URL, CAST(COUNT(DISTINCT plan_handle) AS NVARCHAR(50)) + '' plans are present for a single query in the plan cache - meaning we probably have parameterization issues.'' AS Details FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa WHERE pa.attribute = ''dbid'' GROUP BY qs.query_hash, pa.value - HAVING COUNT(DISTINCT plan_handle) > 50 - ORDER BY COUNT(DISTINCT plan_handle) DESC OPTION (RECOMPILE);'; - + HAVING COUNT(DISTINCT plan_handle) > '; + + IF 50 > (SELECT COUNT(*) FROM sys.databases) + SET @StringToExecute = @StringToExecute + N' 50 '; + ELSE + SELECT @StringToExecute = @StringToExecute + CAST(COUNT(*) * 2 AS NVARCHAR(50)) FROM sys.databases; + + SET @StringToExecute = @StringToExecute + N' ORDER BY COUNT(DISTINCT plan_handle) DESC 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.'; EXECUTE(@StringToExecute); END; - /* Performance - High Number of Cached Plans */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 161 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 161) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) @@ -3751,20 +4754,19 @@ AS 100 AS Priority, ''Performance'' AS FindingsGroup, ''High Number of Cached Plans'' AS Finding, - ''https://BrentOzar.com/go/planlimits'' AS URL, + ''https://www.brentozar.com/go/planlimits'' AS URL, ''Your server configuration is limited to '' + CAST(ht.buckets_count * 4 AS VARCHAR(20)) + '' '' + ht.name + '', and you are currently caching '' + CAST(cc.entries_count AS VARCHAR(20)) + ''.'' AS Details FROM sys.dm_os_memory_cache_hash_tables ht INNER JOIN sys.dm_os_memory_cache_counters cc ON ht.name = cc.name AND ht.type = cc.type where ht.name IN ( ''SQL Plans'' , ''Object Plans'' , ''Bound Trees'' ) AND cc.entries_count >= (3 * ht.buckets_count) 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.'; EXECUTE(@StringToExecute); END; - /* Performance - Too Much Free Memory */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -3780,7 +4782,7 @@ AS Finding, URL, Details) - SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://BrentOzar.com/go/freememory', + SELECT 165, 50, 'Performance', 'Too Much Free Memory', 'https://www.brentozar.com/go/freememory', CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool, which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details FROM sys.dm_os_performance_counters cFree INNER JOIN sys.dm_os_performance_counters cTotal ON cTotal.object_name LIKE N'%Memory Manager%' @@ -3793,14 +4795,13 @@ AS END; - /* Outdated sp_Blitz - sp_Blitz is Over 6 Months Old */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 155 ) AND DATEDIFF(MM, @VersionDate, GETDATE()) > 6 BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 155) WITH NOWAIT; INSERT INTO #BlitzResults @@ -3819,7 +4820,6 @@ AS 'Some things get better with age, like fine wine and your T-SQL. However, sp_Blitz is not one of those things - time to go download the current one.' AS Details; END; - /* Populate a list of database defaults. I'm doing this kind of oddly - it reads like a lot of work, but this way it compiles & runs on all versions of SQL Server. @@ -3828,63 +4828,78 @@ AS IF @Debug IN (1, 2) RAISERROR('Generating database defaults.', 0, 1) WITH NOWAIT; INSERT INTO #DatabaseDefaults - SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_supplemental_logging_enabled', 0, 131, 210, 'Supplemental Logging Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_supplemental_logging_enabled' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'snapshot_isolation_state', 0, 132, 210, 'Snapshot Isolation Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'snapshot_isolation_state' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_read_committed_snapshot_on', 0, 133, 210, 'Read Committed Snapshot Isolation Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_read_committed_snapshot_on', + CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END, /* RCSI is always enabled in Azure SQL DB per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + 133, 210, CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 'Read Committed Snapshot Isolation Disabled' ELSE 'Read Committed Snapshot Isolation Enabled' END, 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_read_committed_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_auto_create_stats_incremental_on', 0, 134, 210, 'Auto Create Stats Incremental Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_auto_create_stats_incremental_on' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_ansi_null_default_on', 0, 135, 210, 'ANSI NULL Default Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_ansi_null_default_on' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_recursive_triggers_on', 0, 136, 210, 'Recursive Triggers Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_recursive_triggers_on' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_trustworthy_on', 0, 137, 210, 'Trustworthy Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_trustworthy_on' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_broker_enabled', 0, 230, 210, 'Broker Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_broker_enabled' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_honor_broker_priority_on', 0, 231, 210, 'Honor Broker Priority Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_honor_broker_priority_on' AND object_id = OBJECT_ID('sys.databases'); + INSERT INTO #DatabaseDefaults + SELECT 'is_parameterization_forced', 0, 138, 210, 'Forced Parameterization Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_parameterization_forced' AND object_id = OBJECT_ID('sys.databases'); /* Not alerting for this since we actually want it and we have a separate check for it: INSERT INTO #DatabaseDefaults - SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_query_store_on', 0, 139, 210, 'Query Store Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_query_store_on' AND object_id = OBJECT_ID('sys.databases'); */ INSERT INTO #DatabaseDefaults - SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'is_cdc_enabled', 0, 140, 210, 'Change Data Capture Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'is_cdc_enabled' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'containment', 0, 141, 210, 'Containment Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'containment' AND object_id = OBJECT_ID('sys.databases'); + --INSERT INTO #DatabaseDefaults + -- SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://www.brentozar.com/go/dbdefaults', NULL + -- FROM sys.all_columns + -- WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'target_recovery_time_in_seconds', 0, 142, 210, 'Target Recovery Time Changed', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'target_recovery_time_in_seconds' AND object_id = OBJECT_ID('sys.databases'); - INSERT INTO #DatabaseDefaults - SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns + SELECT 'delayed_durability', 0, 143, 210, 'Delayed Durability Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns WHERE name = 'delayed_durability' AND object_id = OBJECT_ID('sys.databases'); INSERT INTO #DatabaseDefaults - SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://BrentOzar.com/go/dbdefaults', NULL - FROM sys.all_columns - WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases'); + SELECT 'is_memory_optimized_elevate_to_snapshot_on', 0, 144, 210, 'Memory Optimized Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_memory_optimized_elevate_to_snapshot_on' AND object_id = OBJECT_ID('sys.databases') + AND SERVERPROPERTY('EngineEdition') <> 8; /* Hekaton is always enabled in Managed Instances per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ + INSERT INTO #DatabaseDefaults + SELECT 'is_accelerated_database_recovery_on', 0, 145, 210, 'Acclerated Database Recovery Enabled', 'https://www.brentozar.com/go/dbdefaults', NULL + FROM sys.all_columns + WHERE name = 'is_accelerated_database_recovery_on' AND object_id = OBJECT_ID('sys.databases') AND SERVERPROPERTY('EngineEdition') NOT IN (5, 8) ; DECLARE DatabaseDefaultsLoop CURSOR FOR SELECT name, DefaultValue, CheckID, Priority, Finding, URL, Details @@ -3893,7 +4908,7 @@ AS OPEN DatabaseDefaultsLoop; FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; WHILE @@FETCH_STATUS = 0 - BEGIN + BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, @CurrentCheckID) WITH NOWAIT; @@ -3902,23 +4917,125 @@ 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.[' + @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.[' + @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.'; EXEC (@StringToExecute); - FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; + FETCH NEXT FROM DatabaseDefaultsLoop into @CurrentName, @CurrentDefaultValue, @CurrentCheckID, @CurrentPriority, @CurrentFinding, @CurrentURL, @CurrentDetails; END; 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 + AND NOT EXISTS + ( + SELECT + 1/0 + FROM #SkipChecks AS sc + WHERE sc.DatabaseName IS NULL + AND sc.CheckID = 257 + ) + BEGIN + IF EXISTS + ( + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.name = 'target_recovery_time_in_seconds' + AND ac.object_id = OBJECT_ID('sys.databases') + ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 257) WITH NOWAIT; + + DECLARE + @tri nvarchar(max) = N' + SELECT + DatabaseName = + d.name, + CheckId = + 257, + Priority = + 50, + FindingsGroup = + N''Performance'', + Finding = + N''Recovery Interval Not Optimal'', + URL = + N''https://sqlperformance.com/2020/05/system-configuration/0-to-60-switching-to-indirect-checkpoints'', + Details = + N''The database '' + + QUOTENAME(d.name) + + N'' has a target recovery interval of '' + + RTRIM(d.target_recovery_time_in_seconds) + + CASE + WHEN d.target_recovery_time_in_seconds = 0 + THEN N'', which is a legacy default, and should be changed to 60.'' + WHEN d.target_recovery_time_in_seconds <> 0 + THEN N'', which is probably a mistake, and should be changed to 60.'' + END + FROM sys.databases AS d + WHERE d.database_id > 4 + AND d.is_read_only = 0 + AND d.is_in_standby = 0 + AND d.target_recovery_time_in_seconds <> 60; + '; + + INSERT INTO + #BlitzResults + ( + DatabaseName, + CheckID, + Priority, + FindingsGroup, + Finding, + URL, + Details + ) + EXEC sys.sp_executesql + @tri; + + END; + END; /*This checks to see if Agent is Offline*/ @@ -3931,9 +5048,9 @@ IF @ProductVersionMajor >= 10 FROM sys.all_objects WHERE name = 'dm_server_services' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 167) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -3956,22 +5073,20 @@ IF @ProductVersionMajor >= 10 AND [servicename] LIKE 'SQL Server Agent%' AND CAST(SERVERPROPERTY('Edition') AS VARCHAR(1000)) NOT LIKE '%xpress%'; - END; + END; END; - -/*This checks to see if the Full Text thingy is offline*/ -IF @ProductVersionMajor >= 10 +/* CheckID 258 - Security - SQL Server Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) AND NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 168 ) - BEGIN - IF EXISTS ( SELECT 1 - FROM sys.all_objects - WHERE name = 'dm_server_services' ) + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 258 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE [name] = 'dm_server_services' ) BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 258) WITH NOWAIT; + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -3981,23 +5096,97 @@ IF @ProductVersionMajor >= 10 [Details] ) SELECT - 168 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , - '' AS [URL] , - ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' - ) AS [Details] + 258 AS [CheckID] , + 1 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Dangerous Service Account' AS [Finding] , + 'https://vladdba.com/SQLServerSvcAccount' AS [URL] , + 'SQL Server''s service account is '+ [service_account] + +' - meaning that anyone who can use xp_cmdshell can do absolutely anything on the host.' AS [Details] FROM [sys].[dm_server_services] - WHERE [status_desc] <> 'Running' - AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; - + WHERE ([service_account] = 'LocalSystem' + OR LOWER([service_account]) = 'nt authority\system') + AND [servicename] LIKE 'SQL Server%' + AND [servicename] NOT LIKE 'SQL Server Agent%'; + END; + END; + +/* CheckID 259 - Security - SQL Server Agent Service is running as LocalSystem or NT AUTHORITY\SYSTEM */ +IF (@ProductVersionMajor >= 10 AND @IsWindowsOperatingSystem = 1) + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 259 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE [name] = 'dm_server_services' ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 259) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 259 AS [CheckID] , + 1 AS [Priority] , + 'Security' AS [FindingsGroup] , + 'Dangerous Service Account' AS [Finding] , + 'https://vladdba.com/SQLServerSvcAccount' AS [URL] , + 'SQL Server Agent''s service account is '+ [service_account] + +' - meaning that anyone who can create and run jobs can do absolutely anything on the host.' AS [Details] + FROM + [sys].[dm_server_services] + WHERE ([service_account] = 'LocalSystem' + OR LOWER([service_account]) = 'nt authority\system') + AND [servicename] LIKE 'SQL Server Agent%'; + END; + END; + +/*This checks to see if the Full Text thingy is offline*/ +IF @ProductVersionMajor >= 10 + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 168 ) + BEGIN + IF EXISTS ( SELECT 1 + FROM sys.all_objects + WHERE name = 'dm_server_services' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 168) WITH NOWAIT; + + INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [URL] , + [Details] ) + + SELECT + 168 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Full-text Filter Daemon Launcher is Currently Offline' AS [Finding] , + '' AS [URL] , + ( 'Oops! It looks like the ' + [servicename] + ' service is ' + [status_desc] + '. The startup type is ' + [startup_type_desc] + '.' + ) AS [Details] + FROM + [sys].[dm_server_services] + WHERE [status_desc] <> 'Running' + AND [servicename] LIKE 'SQL Full-text Filter Daemon Launcher%'; + + END; END; - 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 ) @@ -4007,9 +5196,9 @@ IF @ProductVersionMajor >= 10 FROM sys.all_objects WHERE name = 'dm_server_services' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 169) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4023,20 +5212,21 @@ IF @ProductVersionMajor >= 10 250 AS [Priority] , 'Informational' AS [FindingsGroup] , 'SQL Server is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' + 'https://www.brentozar.com/go/setup' AS [URL] , + ( 'I''m running as ' + [service_account] + '.' ) AS [Details] FROM [sys].[dm_server_services] WHERE [service_account] LIKE 'NT Service%' AND [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%'; END; 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 ) @@ -4046,9 +5236,9 @@ IF @ProductVersionMajor >= 10 FROM sys.all_objects WHERE name = 'dm_server_services' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 170) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4062,16 +5252,521 @@ IF @ProductVersionMajor >= 10 250 AS [Priority] , 'Informational' AS [FindingsGroup] , 'SQL Server Agent is running under an NT Service account' AS [Finding] , - 'https://BrentOzar.com/go/setup' AS [URL] , - ( 'I''m running as ' + [service_account] + '. I wish I had an Active Directory service account instead.' + 'https://www.brentozar.com/go/setup' AS [URL] , + ( 'I''m running as ' + [service_account] + '.' ) AS [Details] FROM [sys].[dm_server_services] WHERE [service_account] LIKE 'NT Service%' AND [servicename] LIKE 'SQL Server Agent%'; - END; END; + END; + +/*This checks that First Responder Kit is consistent. +It assumes that all the objects of the kit resides in the same database, the one in which this SP is stored +It also is ready to check for installation in another schema. +*/ +IF( + NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 226 + ) +) +BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running check with id %d',0,1,2000); + + SET @spBlitzFullName = QUOTENAME(DB_NAME()) + '.' +QUOTENAME(OBJECT_SCHEMA_NAME(@@PROCID)) + '.' + QUOTENAME(OBJECT_NAME(@@PROCID)); + SET @BlitzIsOutdatedComparedToOthers = 0; + SET @tsql = NULL; + SET @VersionCheckModeExistsTSQL = NULL; + SET @BlitzProcDbName = DB_NAME(); + SET @ExecRet = NULL; + SET @InnerExecRet = NULL; + SET @TmpCnt = NULL; + + SET @PreviousComponentName = NULL; + SET @PreviousComponentFullPath = NULL; + SET @CurrentStatementId = NULL; + SET @CurrentComponentSchema = NULL; + SET @CurrentComponentName = NULL; + SET @CurrentComponentType = NULL; + SET @CurrentComponentVersionDate = NULL; + SET @CurrentComponentFullName = NULL; + SET @CurrentComponentMandatory = NULL; + SET @MaximumVersionDate = NULL; + + SET @StatementCheckName = NULL; + SET @StatementOutputsCounter = NULL; + SET @OutputCounterExpectedValue = NULL; + SET @StatementOutputsExecRet = NULL; + SET @StatementOutputsDateTime = NULL; + + SET @CurrentComponentMandatoryCheckOK = NULL; + SET @CurrentComponentVersionCheckModeOK = NULL; + + SET @canExitLoop = 0; + SET @frkIsConsistent = 0; + + + SET @tsql = 'USE ' + QUOTENAME(@BlitzProcDbName) + ';' + @crlf + + 'WITH FRKComponents (' + @crlf + + ' ObjectName,' + @crlf + + ' ObjectType,' + @crlf + + ' MandatoryComponent' + @crlf + + ')' + @crlf + + 'AS (' + @crlf + + ' SELECT ''sp_AllNightLog'',''P'' ,0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''sp_AllNightLog_Setup'', ''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_Blitz'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzBackups'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzCache'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzFirst'',''P'',0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''sp_BlitzIndex'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzLock'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzQueryStore'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_BlitzWho'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_DatabaseRestore'',''P'',0' + @crlf + + ' UNION ALL ' + @crlf + + ' SELECT ''sp_ineachdb'',''P'',0' + @crlf + + ' UNION ALL' + @crlf + + ' SELECT ''SqlServerVersions'',''U'',0' + @crlf + + ')' + @crlf + + 'INSERT INTO #FRKObjects (' + @crlf + + ' DatabaseName,ObjectSchemaName,ObjectName, ObjectType,MandatoryComponent' + @crlf + + ')' + @crlf + + 'SELECT DB_NAME(),SCHEMA_NAME(o.schema_id), c.ObjectName,c.ObjectType,c.MandatoryComponent' + @crlf + + 'FROM ' + @crlf + + ' FRKComponents c' + @crlf + + 'LEFT JOIN ' + @crlf + + ' sys.objects o' + @crlf + + 'ON c.ObjectName = o.[name]' + @crlf + + 'AND c.ObjectType = o.[type]' + @crlf + + --'WHERE o.schema_id IS NOT NULL' + @crlf + + ';' + ; + + EXEC @ExecRet = sp_executesql @tsql ; + + -- TODO: add check for statement success + + -- TODO: based on SP requirements and presence (SchemaName is not null) ==> update MandatoryComponent column + + -- Filling #StatementsToRun4FRKVersionCheck + INSERT INTO #StatementsToRun4FRKVersionCheck ( + CheckName,StatementText,SubjectName,SubjectFullPath, StatementOutputsCounter,OutputCounterExpectedValue,StatementOutputsExecRet,StatementOutputsDateTime + ) + SELECT + 'Mandatory', + 'SELECT @cnt = COUNT(*) FROM #FRKObjects WHERE ObjectSchemaName IS NULL AND ObjectName = ''' + ObjectName + ''' AND MandatoryComponent = 1;', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 1, + 0, + 0, + 0 + FROM #FRKObjects + UNION ALL + SELECT + 'VersionCheckMode', + 'SELECT @cnt = COUNT(*) FROM ' + + QUOTENAME(DatabaseName) + '.sys.all_parameters ' + + 'where object_id = OBJECT_ID(''' + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName) + ''') AND [name] = ''@VersionCheckMode'';', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 1, + 1, + 0, + 0 + FROM #FRKObjects + WHERE ObjectType = 'P' + AND ObjectSchemaName IS NOT NULL + UNION ALL + SELECT + 'VersionCheck', + 'EXEC @ExecRet = ' + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName) + ' @VersionCheckMode = 1 , @VersionDate = @ObjDate OUTPUT;', + ObjectName, + QUOTENAME(DatabaseName) + '.' + QUOTENAME(ObjectSchemaName) + '.' + QUOTENAME(ObjectName), + 0, + 0, + 1, + 1 + FROM #FRKObjects + WHERE ObjectType = 'P' + AND ObjectSchemaName IS NOT NULL + ; + IF(@Debug in (1,2)) + BEGIN + SELECT * + FROM #StatementsToRun4FRKVersionCheck ORDER BY SubjectName,SubjectFullPath,StatementId -- in case of schema change ; + END; + + + -- loop on queries... + WHILE(@canExitLoop = 0) + BEGIN + SET @CurrentStatementId = NULL; + + SELECT TOP 1 + @StatementCheckName = CheckName, + @CurrentStatementId = StatementId , + @CurrentComponentName = SubjectName, + @CurrentComponentFullName = SubjectFullPath, + @tsql = StatementText, + @StatementOutputsCounter = StatementOutputsCounter, + @OutputCounterExpectedValue = OutputCounterExpectedValue , + @StatementOutputsExecRet = StatementOutputsExecRet, + @StatementOutputsDateTime = StatementOutputsDateTime + FROM #StatementsToRun4FRKVersionCheck + ORDER BY SubjectName, SubjectFullPath,StatementId /* in case of schema change */ + ; + + -- loop exit condition + IF(@CurrentStatementId IS NULL) + BEGIN + BREAK; + END; + + IF @Debug IN (1, 2) RAISERROR(' Statement: %s',0,1,@tsql); + + -- we start a new component + IF(@PreviousComponentName IS NULL OR + (@PreviousComponentName IS NOT NULL AND @PreviousComponentName <> @CurrentComponentName) OR + (@PreviousComponentName IS NOT NULL AND @PreviousComponentName = @CurrentComponentName AND @PreviousComponentFullPath <> @CurrentComponentFullName) + ) + BEGIN + -- reset variables + SET @CurrentComponentMandatoryCheckOK = 0; + SET @CurrentComponentVersionCheckModeOK = 0; + SET @PreviousComponentName = @CurrentComponentName; + SET @PreviousComponentFullPath = @CurrentComponentFullName ; + END; + + IF(@StatementCheckName NOT IN ('Mandatory','VersionCheckMode','VersionCheck')) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (code generator changed)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Your version check failed because a change has been made to the version check code generator.' + @crlf + + 'Error: No handler for check with name "' + ISNULL(@StatementCheckName,'') + '"' AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + IF(@StatementCheckName = 'Mandatory') + BEGIN + -- outputs counter + EXEC @ExecRet = sp_executesql @tsql, N'@cnt INT OUTPUT',@cnt = @TmpCnt OUTPUT; + + IF(@ExecRet <> 0) + BEGIN + + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Your version check failed due to dynamic query failure.' + @crlf + + 'Error: following query failed at execution (check if component [' + ISNULL(@CurrentComponentName,@CurrentComponentName) + '] is mandatory and missing)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + IF(@TmpCnt <> @OutputCounterExpectedValue) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 227 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Missing: ' + @CurrentComponentName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated version of the First Responder Kit to install it.' AS Details + ; + + -- as it's missing, no value for SubjectFullPath + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectName = @CurrentComponentName ; + CONTINUE; + END; + + SET @CurrentComponentMandatoryCheckOK = 1; + END; + + IF(@StatementCheckName = 'VersionCheckMode') + BEGIN + IF(@CurrentComponentMandatoryCheckOK = 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (unexpectedly modified checks ordering)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because "Mandatory" check has not been completed before for current component' + @crlf + + 'Error: version check mode happenned before "Mandatory" check for component called "' + @CurrentComponentFullName + '"' + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + -- outputs counter + EXEC @ExecRet = sp_executesql @tsql, N'@cnt INT OUTPUT',@cnt = @TmpCnt OUTPUT; + + IF(@ExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because a change has been made to the code generator.' + @crlf + + 'Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] can run in VersionCheckMode)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + IF(@TmpCnt <> @OutputCounterExpectedValue) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 228 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Outdated: ' + @CurrentComponentFullName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Component ' + @CurrentComponentFullName + ' is not at the minimum version required to run this procedure' + @crlf + + 'VersionCheckMode has been introduced in component version date after "20190320". This means its version is lower than or equal to that date.' AS Details; + ; + + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; + + SET @CurrentComponentVersionCheckModeOK = 1; + END; + + IF(@StatementCheckName = 'VersionCheck') + BEGIN + IF(@CurrentComponentMandatoryCheckOK = 0 OR @CurrentComponentVersionCheckModeOK = 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (unexpectedly modified checks ordering)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Version check failed because "VersionCheckMode" check has not been completed before for component called "' + @CurrentComponentFullName + '"' + @crlf + + 'Error: VersionCheck happenned before "VersionCheckMode" check for component called "' + @CurrentComponentFullName + '"' + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + EXEC @ExecRet = sp_executesql @tsql , N'@ExecRet INT OUTPUT, @ObjDate DATETIME OUTPUT', @ExecRet = @InnerExecRet OUTPUT, @ObjDate = @CurrentComponentVersionDate OUTPUT; + + IF(@ExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (dynamic query failure)' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. The version check failed because a change has been made to the code generator.' + @crlf + + 'Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] is at the expected version)' + @crlf + + @tsql AS Details + ; + + -- we will stop the test because it's possible to get the same message for other components + SET @canExitLoop = 1; + CONTINUE; + END; + + + IF(@InnerExecRet <> 0) + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 226 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Version Check Failed (Failed dynamic SP call to ' + @CurrentComponentFullName + ')' AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download an updated First Responder Kit. Error: following query failed at execution (check if component [' + @CurrentComponentFullName + '] is at the expected version)' + @crlf + + 'Return code: ' + CONVERT(VARCHAR(10),@InnerExecRet) + @crlf + + 'T-SQL Query: ' + @crlf + + @tsql AS Details + ; + + -- advance to next component + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; + + IF(@CurrentComponentVersionDate < @VersionDate) + BEGIN + + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 228 AS CheckID , + 253 AS Priority , + 'First Responder Kit' AS FindingsGroup , + 'Component Outdated: ' + @CurrentComponentFullName AS Finding , + 'http://FirstResponderKit.org' AS URL , + 'Download and install the latest First Responder Kit - you''re running some older code, and it doesn''t get better with age.' AS Details + ; + + RAISERROR('Component %s is outdated',10,1,@CurrentComponentFullName); + -- advance to next component + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE SubjectFullPath = @CurrentComponentFullName ; + CONTINUE; + END; + + ELSE IF(@CurrentComponentVersionDate > @VersionDate AND @BlitzIsOutdatedComparedToOthers = 0) + BEGIN + SET @BlitzIsOutdatedComparedToOthers = 1; + RAISERROR('Procedure %s is outdated',10,1,@spBlitzFullName); + IF(@MaximumVersionDate IS NULL OR @MaximumVersionDate < @CurrentComponentVersionDate) + BEGIN + SET @MaximumVersionDate = @CurrentComponentVersionDate; + END; + END; + /* Kept for debug purpose: + ELSE + BEGIN + INSERT INTO #BlitzResults( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 2000 AS CheckID , + 250 AS Priority , + 'Informational' AS FindingsGroup , + 'First Responder kit component ' + @CurrentComponentFullName + ' is at the expected version' AS Finding , + 'https://www.BrentOzar.com/blitz/' AS URL , + 'Version date is: ' + CONVERT(VARCHAR(32),@CurrentComponentVersionDate,121) AS Details + ; + END; + */ + END; + + -- could be performed differently to minimize computation + DELETE FROM #StatementsToRun4FRKVersionCheck WHERE StatementId = @CurrentStatementId ; + END; +END; + /*This counts memory dumps and gives min and max date of in view*/ IF @ProductVersionMajor >= 10 @@ -4084,12 +5779,12 @@ IF @ProductVersionMajor >= 10 FROM sys.all_objects WHERE name = 'dm_server_memory_dumps' ) BEGIN - IF 5 <= (SELECT COUNT(*) FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) + IF EXISTS (SELECT * FROM [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(YEAR, -1, GETDATE())) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 171) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4103,9 +5798,9 @@ IF @ProductVersionMajor >= 10 20 AS [Priority] , 'Reliability' AS [FindingsGroup] , 'Memory Dumps Have Occurred' AS [Finding] , - 'https://BrentOzar.com/go/dump' AS [URL] , - ( 'That ain''t good. I''ve had ' + - CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + + 'https://www.brentozar.com/go/dump' AS [URL] , + ( 'That ain''t good. I''ve had ' + + CAST(COUNT(*) AS VARCHAR(100)) + ' memory dumps between ' + CAST(CAST(MIN([creation_time]) AS DATETIME) AS VARCHAR(100)) + ' and ' + CAST(CAST(MAX([creation_time]) AS DATETIME) AS VARCHAR(100)) + @@ -4115,7 +5810,7 @@ IF @ProductVersionMajor >= 10 [sys].[dm_server_memory_dumps] WHERE [creation_time] >= DATEADD(year, -1, GETDATE()); - END; + END; END; END; @@ -4124,9 +5819,9 @@ IF @ProductVersionMajor >= 10 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 173 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 173) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4140,8 +5835,8 @@ IF @ProductVersionMajor >= 10 200 AS [Priority] , 'Licensing' AS [FindingsGroup] , 'Non-Production License' AS [Finding] , - 'https://BrentOzar.com/go/licensing' AS [URL] , - ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + + 'https://www.brentozar.com/go/licensing' AS [URL] , + ( 'We''re not the licensing police, but if this is supposed to be a production server, and you''re running ' + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) + ' the good folks at Microsoft might get upset with you. Better start counting those cores.' ) AS [Details] @@ -4151,14 +5846,14 @@ IF @ProductVersionMajor >= 10 END; /*Checks to see if Buffer Pool Extensions are in use*/ - IF @ProductVersionMajor >= 12 + IF @ProductVersionMajor >= 12 AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 174 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 174) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4172,8 +5867,8 @@ IF @ProductVersionMajor >= 10 200 AS [Priority] , 'Performance' AS [FindingsGroup] , 'Buffer Pool Extensions Enabled' AS [Finding] , - 'https://BrentOzar.com/go/bpe' AS [URL] , - ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + + 'https://www.brentozar.com/go/bpe' AS [URL] , + ( 'You have Buffer Pool Extensions enabled, and one lives here: ' + [path] + '. It''s currently ' + CASE WHEN [current_size_in_kb] / 1024. / 1024. > 0 @@ -4212,11 +5907,11 @@ IF @ProductVersionMajor >= 10 170 AS Priority , 'File Configuration' AS FindingsGroup , 'TempDB Has >16 Data Files' AS Finding , - 'https://BrentOzar.com/go/tempdb' AS URL , + 'https://www.brentozar.com/go/tempdb' AS URL , 'Woah, Nelly! TempDB has ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + '. Did you forget to terminate a loop somewhere?' AS Details - FROM sys.[master_files] AS [mf] + FROM sys.[master_files] AS [mf] WHERE [mf].[database_id] = 2 AND [mf].[type] = 0 - HAVING COUNT_BIG(*) > 16; + HAVING COUNT_BIG(*) > 16; END; IF NOT EXISTS ( SELECT 1 @@ -4247,13 +5942,18 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Monitoring' AS FindingsGroup , 'Extended Events Hyperextension' AS Finding , - 'https://BrentOzar.com/go/xe' AS URL , + 'https://www.brentozar.com/go/xe' AS URL , 'Hey big spender, you have ' + CAST(COUNT_BIG(*) AS VARCHAR(30)) + ' Extended Events sessions running. You sure you meant to do that?' AS Details FROM sys.dm_xe_sessions WHERE [name] NOT IN - ( 'AlwaysOn_health', 'system_health', 'telemetry_xevents', 'sp_server_diagnostics', 'hkenginexesession' ) + ( 'AlwaysOn_health', + 'system_health', + 'telemetry_xevents', + 'sp_server_diagnostics', + 'sp_server_diagnostics session', + 'hkenginexesession' ) AND name NOT LIKE '%$A%' - HAVING COUNT_BIG(*) >= 2; + HAVING COUNT_BIG(*) >= 2; END; END; @@ -4292,7 +5992,7 @@ IF @ProductVersionMajor >= 10 [sys].[dm_server_registry] AS [dsr] WHERE [dsr].[registry_key] LIKE N'%MSSQLServer\Parameters' - AND [dsr].[value_data] = '-x';; + AND [dsr].[value_data] = '-x';; END; END; @@ -4302,9 +6002,9 @@ IF @ProductVersionMajor >= 10 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 179 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 179) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4320,12 +6020,17 @@ IF @ProductVersionMajor >= 10 'Dangerous Third Party Modules' AS [Finding] , 'https://support.microsoft.com/en-us/kb/2033238' AS [URL] , ( COALESCE(company, '') + ' - ' + COALESCE(description, '') + ' - ' + COALESCE(name, '') + ' - suspected dangerous third party module is installed.') AS [Details] - FROM sys.dm_os_loaded_modules - WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') /* McAfee VirusScan Enterprise */ + FROM sys.dm_os_loaded_modules + WHERE UPPER(name) LIKE UPPER('%\ENTAPI.DLL') OR UPPER(name) LIKE '%MFEBOPK.SYS' /* McAfee VirusScan Enterprise */ OR UPPER(name) LIKE UPPER('%\HIPI.DLL') OR UPPER(name) LIKE UPPER('%\HcSQL.dll') OR UPPER(name) LIKE UPPER('%\HcApi.dll') OR UPPER(name) LIKE UPPER('%\HcThe.dll') /* McAfee Host Intrusion */ - OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') /* Sophos AV */ - OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL'); /* OSISoft PI data access */ - + OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED.DLL') OR UPPER(name) LIKE UPPER('%\SOPHOS_DETOURED_x64.DLL') OR UPPER(name) LIKE UPPER('%\SWI_IFSLSP_64.dll') OR UPPER(name) LIKE UPPER('%\SOPHOS~%.dll') /* Sophos AV */ + OR UPPER(name) LIKE UPPER('%\PIOLEDB.DLL') OR UPPER(name) LIKE UPPER('%\PISDK.DLL') /* OSISoft PI data access */ + OR UPPER(name) LIKE UPPER('%ScriptControl%.dll') OR UPPER(name) LIKE UPPER('%umppc%.dll') /* CrowdStrike */ + OR UPPER(name) LIKE UPPER('%perfiCrcPerfMonMgr.DLL') /* Trend Micro OfficeScan */ + OR UPPER(name) LIKE UPPER('%NLEMSQL.SYS') /* NetLib Encryptionizer-Software. */ + OR UPPER(name) LIKE UPPER('%MFETDIK.SYS') /* McAfee Anti-Virus Mini-Firewall */ + OR UPPER(name) LIKE UPPER('%ANTIVIRUS%'); /* To pick up sqlmaggieAntiVirus_64.dll (malware) or anything else labelled AntiVirus */ + /* MS docs link for blacklisted modules: https://learn.microsoft.com/en-us/troubleshoot/sql/performance/performance-consistency-issues-filter-drivers-modules */ END; /*Find shrink database tasks*/ @@ -4352,7 +6057,7 @@ IF @ProductVersionMajor >= 10 [FindingsGroup] , [Finding] , [URL] , - [Details] ) + [Details] ) SELECT 180 AS [CheckID] , -- sp_Blitz Issue #776 @@ -4364,24 +6069,24 @@ IF @ProductVersionMajor >= 10 END AS Priority, 'Performance' AS [FindingsGroup] , 'Shrink Database Step In Maintenance Plan' AS [Finding] , - 'https://BrentOzar.com/go/autoshrink' AS [URL] , - 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' + 'https://www.brentozar.com/go/autoshrink' AS [URL] , + 'The maintenance plan ' + [mps].[name] + ' has a step to shrink databases in it. Shrinking databases is as outdated as maintenance plans.' + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS [Details] FROM [maintenance_plan_steps] [mps] CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) - join msdb.dbo.sysmaintplan_subplans as sms - on mps.id = sms.plan_id - JOIN msdb.dbo.sysjobs j + join msdb.dbo.sysmaintplan_subplans as sms + on mps.id = sms.plan_id + JOIN msdb.dbo.sysjobs j on sms.job_id = j.job_id LEFT OUTER JOIN msdb.dbo.sysjobsteps AS step ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc + LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc + LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc ON sjsc.schedule_id = ssc.schedule_id AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id + LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh + ON j.job_id = sjh.job_id AND step.step_id = sjh.step_id AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time @@ -4389,7 +6094,6 @@ IF @ProductVersionMajor >= 10 END; - /*Find repetitive maintenance tasks*/ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -4410,7 +6114,7 @@ IF @ProductVersionMajor >= 10 FROM [maintenance_plan_steps] [mps] CROSS APPLY [maintenance_plan_xml].[nodes]('//dts:Executables/dts:Executable') [t]([c]) ), [mp_steps_pretty] AS (SELECT DISTINCT [m1].[name] , - STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] + STUFF((SELECT N', ' + [m2].[step_name] FROM [maintenance_plan_table] AS [m2] WHERE [m1].[name] = [m2].[name] FOR XML PATH(N'')), 1, 2, N'') AS [maintenance_plan_steps] FROM [maintenance_plan_table] AS [m1]) @@ -4427,7 +6131,7 @@ IF @ProductVersionMajor >= 10 100 AS [Priority] , 'Performance' AS [FindingsGroup] , 'Repetitive Steps In Maintenance Plans' AS [Finding] , - 'https://ola.hallengren.com/' AS [URL] , + 'https://ola.hallengren.com/' AS [URL] , 'The maintenance plan ' + [m].[name] + ' is doing repetitive work on indexes and statistics. Perhaps it''s time to try something more modern?' AS [Details] FROM [mp_steps_pretty] m WHERE m.[maintenance_plan_steps] LIKE '%Rebuild%Reorganize%' @@ -4451,14 +6155,14 @@ IF @ProductVersionMajor >= 10 20 AS Priority , ''Reliability'' AS FindingsGroup , ''No Failover Cluster Nodes Available'' AS Finding , - ''https://BrentOzar.com/go/node'' AS URL , + ''https://www.brentozar.com/go/node'' AS URL , ''There are no failover cluster nodes available if the active node fails'' AS Details FROM ( SELECT SUM(CASE WHEN [status] = 0 AND [is_current_owner] = 0 THEN 1 ELSE 0 END) AS [available_nodes] FROM sys.dm_os_cluster_nodes ) a WHERE [available_nodes] < 1 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.'; @@ -4470,6 +6174,8 @@ IF @ProductVersionMajor >= 10 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 191 ) AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> (SELECT COUNT(*) FROM tempdb.sys.database_files) + /* User may have no permissions to see tempdb files in sys.master_files. In that case count returned will be 0 and we want to skip the check */ + AND (SELECT COUNT(*) FROM sys.master_files WHERE database_id = 2) <> 0 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 191) WITH NOWAIT @@ -4487,7 +6193,7 @@ IF @ProductVersionMajor >= 10 50 AS [Priority] , 'Reliability' AS [FindingsGroup] , 'TempDB File Error' AS [Finding] , - 'https://BrentOzar.com/go/tempdboops' AS [URL] , + 'https://www.brentozar.com/go/tempdboops' AS [URL] , 'Mismatch between the number of TempDB files in sys.master_files versus tempdb.sys.database_files' AS [Details]; END; @@ -4507,7 +6213,7 @@ IF @ProductVersionMajor >= 10 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 198) WITH NOWAIT - + INSERT INTO #BlitzResults ( CheckID, @@ -4523,7 +6229,7 @@ IF @ProductVersionMajor >= 10 10 AS Priority, 'Performance' AS FindingsGroup, 'CPU w/Odd Number of Cores' AS Finding, - 'https://BrentOzar.com/go/oddity' AS URL, + 'https://www.brentozar.com/go/oddity' AS URL, 'Node ' + CONVERT(VARCHAR(10), parent_node_id) + ' has ' + CONVERT(VARCHAR(10), COUNT(cpu_id)) + CASE WHEN COUNT(cpu_id) = 1 THEN ' core assigned to it. This is a really bad NUMA configuration.' ELSE ' cores assigned to it. This is a really bad NUMA configuration.' @@ -4532,9 +6238,17 @@ IF @ProductVersionMajor >= 10 WHERE is_online = 1 AND scheduler_id < 255 AND parent_node_id < 64 + AND EXISTS ( + SELECT 1 + FROM ( SELECT memory_node_id, SUM(online_scheduler_count) AS schedulers + FROM sys.dm_os_nodes + WHERE memory_node_id < 64 + GROUP BY memory_node_id ) AS nodes + HAVING MIN(nodes.schedulers) <> MAX(nodes.schedulers) + ) GROUP BY parent_node_id, is_online - HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; + HAVING ( COUNT(cpu_id) + 2 ) % 2 = 1; END; @@ -4545,37 +6259,37 @@ IF @ProductVersionMajor >= 10 BEGIN SELECT UPPER( REPLACE( - SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, + SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), 0, ISNULL( NULLIF( - CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), - 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. - , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), + CHARINDEX('(', CONVERT(NVARCHAR(MAX), t.TextData)), + 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 )) --This replaces everything up to an open paren, if one exists. + , SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), ISNULL( NULLIF( CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), + , 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ) , '') --This replaces any optional WITH clause to a DBCC command, like tableresults. ) AS [dbcc_event_trunc_upper], UPPER( REPLACE( - CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), + CONVERT(NVARCHAR(MAX), t.TextData), SUBSTRING(CONVERT(NVARCHAR(MAX), t.TextData), ISNULL( NULLIF( CHARINDEX(' WITH ',CONVERT(NVARCHAR(MAX), t.TextData)) - , 0), - LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), + , 0), + LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1), LEN(CONVERT(NVARCHAR(MAX), t.TextData)) + 1 ), '')) AS [dbcc_event_full_upper], MIN(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS min_start_time, MAX(t.StartTime) OVER (PARTITION BY CONVERT(NVARCHAR(128), t.TextData)) AS max_start_time, - t.NTUserName AS [nt_user_name], - t.NTDomainName AS [nt_domain_name], + t.NTUserName AS [nt_user_name], + t.NTDomainName AS [nt_domain_name], t.HostName AS [host_name], - t.ApplicationName AS [application_name], - t.LoginName [login_name], + t.ApplicationName AS [application_name], + t.LoginName [login_name], t.DBUserName AS [db_user_name] INTO #dbcc_events_from_trace FROM #fnTraceGettable AS t @@ -4589,9 +6303,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 203 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 199) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4603,8 +6317,8 @@ IF @ProductVersionMajor >= 10 50 AS Priority , 'DBCC Events' AS FindingsGroup , 'Overall Events' AS Finding , - '' AS URL , - CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + 'https://www.BrentOzar.com/go/dbcc' AS URL , + CAST(COUNT(*) AS NVARCHAR(100)) + ' DBCC events have taken place between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + '. This does not include CHECKDB and other usually benign DBCC events.' AS Details FROM #dbcc_events_from_trace d @@ -4614,6 +6328,7 @@ IF @ProductVersionMajor >= 10 command they're trying to run. See Github issues 1062, 1074, 1075. */ WHERE d.dbcc_event_full_upper NOT LIKE '%DBCC%ADDINSTANCE%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%AUTOPILOT%' AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKALLOC%' AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCATALOG%' AND d.dbcc_event_full_upper NOT LIKE '%DBCC%CHECKCONSTRAINTS%' @@ -4637,11 +6352,15 @@ IF @ProductVersionMajor >= 10 AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEON%' AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACEOFF%' AND d.dbcc_event_full_upper NOT LIKE '%DBCC%TRACESTATUS%' + AND d.dbcc_event_full_upper NOT LIKE '%DBCC%USEROPTIONS%' AND d.application_name NOT LIKE 'Critical Care(R) Collector' AND d.application_name NOT LIKE '%Red Gate Software Ltd SQL Prompt%' AND d.application_name NOT LIKE '%Spotlight Diagnostic Server%' AND d.application_name NOT LIKE '%SQL Diagnostic Manager%' + AND d.application_name NOT LIKE 'SQL Server Checkup%' AND d.application_name NOT LIKE '%Sentry%' + AND d.application_name NOT LIKE '%LiteSpeed%' + AND d.application_name NOT LIKE '%SQL Monitor - Monitoring%' HAVING COUNT(*) > 0; @@ -4654,9 +6373,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 207 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 200) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 207) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4668,8 +6387,8 @@ IF @ProductVersionMajor >= 10 10 AS Priority , 'Performance' AS FindingsGroup , 'DBCC DROPCLEANBUFFERS Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC DROPCLEANBUFFERS ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + '. If this is a production box, know that you''re clearing all data out of memory when this happens. What kind of monster would do that?' AS Details FROM #dbcc_events_from_trace d @@ -4677,7 +6396,7 @@ IF @ProductVersionMajor >= 10 GROUP BY COALESCE(d.nt_user_name, d.login_name) HAVING COUNT(*) > 0; - END; + END; /*Check for someone running free proc cache*/ IF NOT EXISTS ( SELECT 1 @@ -4685,9 +6404,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 208 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 201) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 208) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4699,8 +6418,8 @@ IF @ProductVersionMajor >= 10 10 AS Priority , 'DBCC Events' AS FindingsGroup , 'DBCC FREEPROCCACHE Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC FREEPROCCACHE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + '. This has bad idea jeans written all over its butt, like most other bad idea jeans.' AS Details FROM #dbcc_events_from_trace d @@ -4716,9 +6435,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 205 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 202) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4730,8 +6449,8 @@ IF @ProductVersionMajor >= 10 50 AS Priority , 'Performance' AS FindingsGroup , 'Wait Stats Cleared Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC SQLPERF(''SYS.DM_OS_WAIT_STATS'',CLEAR) ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + '. Why are you clearing wait stats? What are you hiding?' AS Details FROM #dbcc_events_from_trace d @@ -4747,9 +6466,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 209 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 203) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 209) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4758,11 +6477,11 @@ IF @ProductVersionMajor >= 10 [URL] , [Details] ) SELECT 209 AS CheckID , - 50 AS Priority , + 10 AS Priority , 'Reliability' AS FindingsGroup , 'DBCC WRITEPAGE Used Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run DBCC WRITEPAGE ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + '. So, uh, are they trying to fix corruption, or cause corruption?' AS Details FROM #dbcc_events_from_trace d @@ -4777,9 +6496,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 210 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 204) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 210) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4792,9 +6511,9 @@ IF @ProductVersionMajor >= 10 10 AS Priority , 'Performance' AS FindingsGroup , 'DBCC SHRINK% Ran Recently' AS Finding , - '' AS URL , - 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + - '. So, uh, are they trying cause bad performance on purpose?' + 'https://www.BrentOzar.com/go/dbcc' AS URL , + 'The user ' + COALESCE(d.nt_user_name, d.login_name) + ' has run file shrinks ' + CAST(COUNT(*) AS NVARCHAR(100)) + ' times between ' + CONVERT(NVARCHAR(30), MIN(d.min_start_time)) + ' and ' + CONVERT(NVARCHAR(30), MAX(d.max_start_time)) + + '. So, uh, are they trying to cause bad performance on purpose?' AS Details FROM #dbcc_events_from_trace d WHERE d.dbcc_event_trunc_upper LIKE N'DBCC SHRINK%' @@ -4803,7 +6522,6 @@ IF @ProductVersionMajor >= 10 END; - /*End: checking default trace for odd DBCC activity*/ /*Begin check for autoshrink events*/ @@ -4813,9 +6531,9 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 206 ) AND @TraceFileIssue = 0 BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 205) WITH NOWAIT - + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 206) WITH NOWAIT + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -4828,12 +6546,12 @@ IF @ProductVersionMajor >= 10 10 AS Priority , 'Performance' AS FindingsGroup , 'Auto-Shrink Ran Recently' AS Finding , - '' AS URL , - N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' - + CONVERT(NVARCHAR(10), COUNT(*)) - + N' auto shrink events between ' - + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) - + ' that lasted on average ' + '' AS URL , + N'The database ' + QUOTENAME(t.DatabaseName) + N' has had ' + + CONVERT(NVARCHAR(10), COUNT(*)) + + N' auto shrink events between ' + + CONVERT(NVARCHAR(30), MIN(t.StartTime)) + ' and ' + CONVERT(NVARCHAR(30), MAX(t.StartTime)) + + ' that lasted on average ' + CONVERT(NVARCHAR(10), AVG(DATEDIFF(SECOND, t.StartTime, t.EndTime))) + ' seconds.' AS Details FROM #fnTraceGettable AS t @@ -4843,6 +6561,323 @@ IF @ProductVersionMajor >= 10 END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 215 ) + AND @TraceFileIssue = 0 + AND EXISTS (SELECT * FROM sys.all_columns WHERE name = 'database_id' AND object_id = OBJECT_ID('sys.dm_exec_sessions')) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 215) WITH NOWAIT + + SET @StringToExecute = 'INSERT INTO [#BlitzResults] + ( [CheckID] , + [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] ) + + SELECT 215 AS CheckID , + 100 AS Priority , + ''Performance'' AS FindingsGroup , + ''Implicit Transactions'' AS Finding , + DB_NAME(s.database_id) AS DatabaseName, + ''https://www.brentozar.com/go/ImplicitTransactions/'' AS URL , + N''The database '' + + DB_NAME(s.database_id) + + '' has '' + + CONVERT(NVARCHAR(20), COUNT_BIG(*)) + + '' open implicit transactions with an oldest begin time of '' + + CONVERT(NVARCHAR(30), MIN(tat.transaction_begin_time)) + + '' Run sp_BlitzWho and check the is_implicit_transaction column to see the culprits.'' AS details + FROM sys.dm_tran_active_transactions AS tat + LEFT JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + LEFT JOIN sys.dm_exec_sessions AS s + ON s.session_id = tst.session_id + WHERE tat.name = ''implicit_transaction'' + GROUP BY DB_NAME(s.database_id), transaction_type, transaction_state;'; + + + 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.'; + + EXECUTE(@StringToExecute); + + + + END; + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 221 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 221) WITH NOWAIT; + + WITH reboot_airhorn + AS + ( + SELECT create_date + FROM sys.databases + WHERE database_id = 2 + UNION ALL + SELECT CAST(DATEADD(SECOND, ( ms_ticks / 1000 ) * ( -1 ), GETDATE()) AS DATETIME) + FROM sys.dm_os_sys_info + ) + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 221 AS CheckID, + 10 AS Priority, + 'Reliability' AS FindingsGroup, + 'Server restarted in last 24 hours' AS Finding, + '' AS URL, + 'Surprise! Your server was last restarted on: ' + CONVERT(VARCHAR(30), MAX(reboot_airhorn.create_date)) AS details + FROM reboot_airhorn + HAVING MAX(reboot_airhorn.create_date) >= DATEADD(HOUR, -24, GETDATE()); + + + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 229 ) + AND CAST(SERVERPROPERTY('Edition') AS NVARCHAR(4000)) LIKE '%Evaluation%' + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 229) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 229 AS CheckID, + 1 AS Priority, + 'Reliability' AS FindingsGroup, + 'Evaluation Edition' AS Finding, + 'https://www.BrentOzar.com/go/workgroup' AS URL, + 'This server will stop working on: ' + CAST(CONVERT(DATETIME, DATEADD(DD, 180, create_date), 102) AS VARCHAR(100)) AS details + FROM sys.server_principals + WHERE sid = 0x010100000000000512000000; + + END; + + + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 233 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 233) WITH NOWAIT; + + + IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_os_memory_clerks') AND name = 'pages_kb') + BEGIN + /* SQL 2012+ version */ + SET @StringToExecute = N' + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 233 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + ''https://www.BrentOzar.com/go/userstore'' AS URL, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / SUM(pages_kb) >= 0.1 + AND SUM(pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + ELSE + BEGIN + /* Antiques Roadshow SQL 2008R2 - version */ + SET @StringToExecute = N' + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 233 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + ''https://www.BrentOzar.com/go/userstore'' AS URL, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / SUM(single_pages_kb + multi_pages_kb) >= 0.1 + AND SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + + END; + + + + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 234 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 234) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + DatabaseName , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 234 AS CheckID, + 100 AS Priority, + db_name(f.database_id) AS DatabaseName, + 'Reliability' AS FindingsGroup, + 'SQL Server Update May Fail' AS Finding, + 'https://desertdba.com/failovers-cant-serve-two-masters/' AS URL, + 'This database has a file with a logical name of ''master'', which can break SQL Server updates. Rename it in SSMS by right-clicking on the database, go into Properties, and rename the file. Takes effect instantly.' AS details + FROM master.sys.master_files f + WHERE (f.name = N'master') + AND f.database_id > 4 + AND db_name(f.database_id) <> 'master'; /* Thanks Michaels3 for catching this */ + END; + + + + 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 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 @@ -4865,10 +6900,10 @@ IF @ProductVersionMajor >= 10 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 99 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 99) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; 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://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 @@ -4878,7 +6913,6 @@ IF @ProductVersionMajor >= 10 don't output those results in the last step. */ - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 163 ) @@ -4889,6 +6923,7 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 163) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, @@ -4898,25 +6933,157 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT TOP 1 163, - ''?'', + N''?'', 200, ''Performance'', ''Query Store Disabled'', - ''https://BrentOzar.com/go/querystore'', + ''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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + FROM [?].sys.database_query_store_options WHERE desired_state = 0 + AND ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 262 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + AND @ProductVersionMajor > 13 /* The relevant column only exists in 2017+ */ + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 262) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 262, + N''?'', + 200, + ''Performance'', + ''Query Store Wait Stats Disabled'', + ''https://www.sqlskills.com/blogs/erin/query-store-settings/'', + (''The new SQL Server 2017 Query Store feature for tracking wait stats has not been enabled on this database. It is very useful for tracking wait stats at a query level.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND wait_stats_capture_mode = 0 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE)'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 263 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 263) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 263, + N''?'', + 200, + ''Performance'', + ''Query Store Effectively Disabled'', + ''https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify'', + (''Query Store is not in a state where it is writing, so it is effectively disabled. Check your Query Store settings.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND actual_state <> 2 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE)'; END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 264 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 264) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 264, + N''?'', + 200, + ''Performance'', + ''Undesired Query Store State'', + ''https://learn.microsoft.com/en-us/sql/relational-databases/performance/best-practice-with-the-query-store#Verify'', + (''You have asked for Query Store to be in '' + desired_state_desc + '' mode, but it is in '' + actual_state_desc + '' mode.'') + FROM [?].sys.database_query_store_options + WHERE desired_state <> 0 + AND desired_state <> actual_state + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE)'; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 265 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 265) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 265, + N''?'', + 200, + ''Performance'', + ''Query Store Unusually Configured'', + ''https://www.sqlskills.com/blogs/erin/query-store-best-practices/'', + (''The '' + query_capture_mode_desc + '' query capture mode '' + + CASE query_capture_mode_desc + WHEN ''ALL'' THEN ''captures more data than you will probably use. If your workload is heavily ad-hoc, then it can also cause Query Store to capture so much that it turns itself off.'' + WHEN ''NONE'' THEN ''stops Query Store capturing data for new queries.'' + WHEN ''CUSTOM'' THEN ''suggests that somebody has gone out of their way to only capture exactly what they want.'' + ELSE ''is not documented.'' END) + 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; - IF @ProductVersionMajor >= 13 AND @ProductVersionMinor < 2149 --CU1 has the fix in it + IF @ProductVersionMajor = 13 AND @ProductVersionMinor < 2149 --2016 CU1 has the fix in it AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 182 ) AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Enterprise%' AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Developer%' - BEGIN - + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 182) WITH NOWAIT; SET @StringToExecute = 'INSERT INTO #BlitzResults @@ -4927,16 +7094,18 @@ IF @ProductVersionMajor >= 10 Finding, URL, Details) - SELECT TOP 1 + SELECT TOP 1 182, ''Server'', 20, ''Reliability'', ''Query Store Cleanup Disabled'', - ''https://BrentOzar.com/go/cleanup'', + ''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.'; @@ -4944,15 +7113,47 @@ IF @ProductVersionMajor >= 10 EXECUTE(@StringToExecute); END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 235 ) + AND EXISTS(SELECT * FROM sys.all_objects WHERE name = 'database_query_store_options') + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 235) WITH NOWAIT; + + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults + (CheckID, + DatabaseName, + Priority, + FindingsGroup, + Finding, + URL, + Details) + SELECT TOP 1 235, + N''?'', + 150, + ''Performance'', + ''Inconsistent Query Store metadata'', + '''', + (''Query store state in master metadata and database specific metadata not in sync.'') + 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 ''?'' NOT IN (''master'', ''model'', ''msdb'', ''rdsadmin'', ''tempdb'', ''DWConfiguration'', ''DWDiagnostics'', ''DWQueue'', ''ReportServer'', ''ReportServerTempDB'') OPTION (RECOMPILE)'; + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 41 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 41) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -4961,27 +7162,29 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 41, - ''?'', + N''?'', 170, ''File Configuration'', ''Multiple Log Files on One Drive'', - ''https://BrentOzar.com/go/manylogs'', + ''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 ''?'' <> ''[tempdb]'' + AND ''?'' NOT IN (''rdsadmin'',''tempdb'') GROUP BY LEFT(physical_name, 1) - HAVING COUNT(*) > 1 OPTION (RECOMPILE);'; + HAVING COUNT(*) > 1 + AND SUM(size) < 268435456 OPTION (RECOMPILE);'; END; IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 42 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 42) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -4990,19 +7193,19 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT DISTINCT 42, - ''?'', + N''?'', 170, ''File Configuration'', ''Uneven File Growth Settings in One Filegroup'', - ''https://BrentOzar.com/go/grow'', + ''https://www.brentozar.com/go/grow'', (''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; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 82 ) @@ -5011,7 +7214,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 82) WITH NOWAIT; EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5019,18 +7223,18 @@ IF @ProductVersionMajor >= 10 Finding, URL, Details) SELECT DISTINCT 82 AS CheckID, - ''?'' as DatabaseName, + N''?'' as DatabaseName, 170 AS Priority, ''File Configuration'' AS FindingsGroup, ''File growth set to percent'', - ''https://BrentOzar.com/go/percentgrowth'' AS URL, - ''The ['' + DB_NAME() + ''] database file '' + f.physical_name + '' has grown to '' + CONVERT(NVARCHAR(10), 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.'' + ''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 */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -5040,7 +7244,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 158) WITH NOWAIT; EXEC sp_MSforeachdb 'use [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5048,29 +7253,31 @@ IF @ProductVersionMajor >= 10 Finding, URL, Details) SELECT DISTINCT 158 AS CheckID, - ''?'' as DatabaseName, + N''?'' as DatabaseName, 170 AS Priority, ''File Configuration'' AS FindingsGroup, ''File growth set to 1MB'', - ''https://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((f.size * 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);'; + ''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 + AND ''?'' != ''rdsadmin'' + OPTION (RECOMPILE);'; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 33 ) BEGIN IF @@VERSION NOT LIKE '%Microsoft SQL Server 2000%' AND @@VERSION NOT LIKE '%Microsoft SQL Server 2005%' + AND @SkipBlockingChecks = 0 BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 33) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5083,19 +7290,20 @@ IF @ProductVersionMajor >= 10 200, ''Licensing'', ''Enterprise Edition Features In Use'', - ''https://BrentOzar.com/go/ee'', + ''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; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 19 ) BEGIN /* Method 1: Check sys.databases parameters */ - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 19) WITH NOWAIT; INSERT INTO #BlitzResults @@ -5113,21 +7321,22 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Informational' AS FindingsGroup , 'Replication In Use' AS Finding , - 'https://BrentOzar.com/go/repl' AS URL , + 'https://www.brentozar.com/go/repl' AS URL , ( 'Database [' + [name] + '] is a replication publisher, subscriber, or distributor.' ) AS Details FROM sys.databases WHERE name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 19) - AND is_published = 1 + AND (is_published = 1 OR is_subscribed = 1 OR is_merge_published = 1 - OR is_distributor = 1; + OR is_distributor = 1); /* Method B: check subscribers for MSreplication_objects tables */ - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults + EXEC dbo.sp_MSforeachdb 'USE [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5140,15 +7349,14 @@ IF @ProductVersionMajor >= 10 200, ''Informational'', ''Replication In Use'', - ''https://BrentOzar.com/go/repl'', + ''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 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 32 ) @@ -5157,7 +7365,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 32) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5166,48 +7375,19 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 32, - ''?'', + N''?'', 150, ''Performance'', ''Triggers on Tables'', - ''https://BrentOzar.com/go/trig'', + ''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; - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 38 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 38) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 38, - ''?'', - 110, - ''Performance'', - ''Active Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that are being actively queried.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NOT NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' OPTION (RECOMPILE)'; - END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 164 ) @@ -5217,7 +7397,9 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 164) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SET QUOTED_IDENTIFIER ON; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5226,44 +7408,15 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT DISTINCT 164, - ''?'', - 20, + N''?'', + 100, ''Reliability'', ''Plan Guides Failing'', - ''https://BrentOzar.com/go/misguided'', + ''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)'; - END; - - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 39 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 39) WITH NOWAIT; - - EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults - (CheckID, - DatabaseName, - Priority, - FindingsGroup, - Finding, - URL, - Details) - SELECT DISTINCT 39, - ''?'', - 150, - ''Performance'', - ''Inactive Tables Without Clustered Indexes'', - ''https://BrentOzar.com/go/heaps'', - (''The ['' + DB_NAME() + ''] database has heaps - tables without a clustered index - that have not been queried since the last restart. These may be backup tables carelessly left behind.'') - FROM [?].sys.indexes i INNER JOIN [?].sys.objects o ON i.object_id = o.object_id - INNER JOIN [?].sys.partitions p ON i.object_id = p.object_id AND i.index_id = p.index_id - INNER JOIN sys.databases sd ON sd.name = ''?'' - LEFT OUTER JOIN [?].sys.dm_db_index_usage_stats ius ON i.object_id = ius.object_id AND i.index_id = ius.index_id AND ius.database_id = sd.database_id - WHERE i.type_desc = ''HEAP'' AND COALESCE(ius.user_seeks, ius.user_scans, ius.user_lookups, ius.user_updates) IS NULL - AND sd.name <> ''tempdb'' AND sd.name <> ''DWDiagnostics'' AND o.is_ms_shipped = 0 AND o.type <> ''S'' 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 @@ -5274,7 +7427,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 46) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5283,14 +7437,16 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 46, - ''?'', + N''?'', 150, ''Performance'', ''Leftover Fake Indexes From Wizards'', - ''https://BrentOzar.com/go/hypo'', + ''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 @@ -5301,7 +7457,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 47) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5310,17 +7467,16 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 47, - ''?'', + N''?'', 100, ''Performance'', ''Indexes Disabled'', - ''https://BrentOzar.com/go/ixoff'', + ''https://www.brentozar.com/go/ixoff'', (''The index ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is disabled. This index is not actually helping performance and should either be enabled or 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_disabled = 1 OPTION (RECOMPILE);'; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 48 ) @@ -5329,7 +7485,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 48) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5338,14 +7495,16 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT DISTINCT 48, - ''?'', + N''?'', 150, ''Performance'', ''Foreign Keys Not Trusted'', - ''https://BrentOzar.com/go/trust'', + ''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 ''?'' 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 @@ -5356,7 +7515,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 56) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5365,11 +7525,11 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 56, - ''?'', + N''?'', 150, ''Performance'', ''Check Constraint Not Trusted'', - ''https://BrentOzar.com/go/trust'', + ''https://www.brentozar.com/go/trust'', (''The check constraint ['' + DB_NAME() + ''].['' + s.name + ''].['' + o.name + ''].['' + i.name + ''] is not trusted - meaning, it was disabled, data was changed, and then the constraint was enabled again. Simply enabling the constraint is not enough for the optimizer to use this constraint - we have to alter the table using the WITH CHECK CHECK CONSTRAINT parameter.'') from [?].sys.check_constraints 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 @@ -5387,7 +7547,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 95) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5396,11 +7557,11 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT TOP 1 95 AS CheckID, - ''?'' as DatabaseName, + N''?'' as DatabaseName, 110 AS Priority, ''Performance'' AS FindingsGroup, ''Plan Guides Enabled'' AS Finding, - ''https://BrentOzar.com/go/guides'' AS URL, + ''https://www.brentozar.com/go/guides'' AS URL, (''Database ['' + DB_NAME() + ''] has query plan guides so a query will always get a specific execution plan. If you are having trouble getting query performance to improve, it might be due to a frozen plan. Review the DMV sys.plan_guides to learn more about the plan guides in place on this server.'') AS Details FROM [?].sys.plan_guides WHERE is_disabled = 0 OPTION (RECOMPILE);'; END; @@ -5414,7 +7575,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 60) WITH NOWAIT; EXEC sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5423,35 +7585,34 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 60 AS CheckID, - ''?'' as DatabaseName, + N''?'' as DatabaseName, 100 AS Priority, ''Performance'' AS FindingsGroup, ''Fill Factor Changed'', - ''https://BrentOzar.com/go/fillfactor'' AS URL, + ''https://www.brentozar.com/go/fillfactor'' AS URL, ''The ['' + DB_NAME() + ''] database has '' + CAST(SUM(1) AS NVARCHAR(50)) + '' objects with fill factor = '' + CAST(fill_factor AS NVARCHAR(5)) + ''%. This can cause memory and storage performance problems, but may also prevent page splits.'' FROM [?].sys.indexes - WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 + WHERE fill_factor <> 0 AND fill_factor < 80 AND is_disabled = 0 AND is_hypothetical = 0 GROUP BY fill_factor OPTION (RECOMPILE);'; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 78 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 78) WITH NOWAIT; - EXECUTE master.sys.sp_MSforeachdb 'USE [?]; - INSERT INTO #Recompile - SELECT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA - FROM sys.sql_modules AS SM - LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() - LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' + EXECUTE master.sys.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #Recompile + SELECT DISTINCT DBName = DB_Name(), SPName = SO.name, SM.is_recompiled, ISR.SPECIFIC_SCHEMA + FROM sys.sql_modules AS SM + LEFT OUTER JOIN master.sys.databases AS sDB ON SM.object_id = DB_id() + LEFT OUTER JOIN dbo.sysobjects AS SO ON SM.object_id = SO.id and type = ''P'' LEFT OUTER JOIN INFORMATION_SCHEMA.ROUTINES AS ISR on ISR.Routine_Name = SO.name AND ISR.SPECIFIC_CATALOG = DB_Name() WHERE SM.is_recompiled=1 OPTION (RECOMPILE); /* oh the rich irony of recompile here */ - '; + '; INSERT INTO #BlitzResults (Priority, FindingsGroup, @@ -5460,19 +7621,19 @@ IF @ProductVersionMajor >= 10 URL, Details, CheckID) - SELECT [Priority] = '100', - FindingsGroup = 'Performance', + SELECT [Priority] = '100', + FindingsGroup = 'Performance', Finding = 'Stored Procedure WITH RECOMPILE', DatabaseName = DBName, - URL = 'https://BrentOzar.com/go/recompile', + URL = 'https://www.brentozar.com/go/recompile', Details = '[' + DBName + '].[' + SPSchema + '].[' + ProcName + '] has WITH RECOMPILE in the stored procedure code, which may cause increased CPU usage due to constant recompiles of the code.', CheckID = '78' - FROM #Recompile AS TR WHERE ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%'; + FROM #Recompile AS TR + WHERE ProcName NOT LIKE 'sp_AllNightLog%' AND ProcName NOT LIKE 'sp_AskBrent%' AND ProcName NOT LIKE 'sp_Blitz%' AND ProcName NOT LIKE 'sp_PressureDetector' + AND DBName NOT IN ('master', 'model', 'msdb', 'tempdb'); DROP TABLE #Recompile; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 86 ) @@ -5480,10 +7641,9 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 86) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://BrentOzar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM [?].dbo.sysmembers m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; + EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 86, DB_NAME(), 230, ''Security'', ''Elevated Permissions on a Database'', ''https://www.brentozar.com/go/elevated'', (''In ['' + DB_NAME() + ''], user ['' + u.name + ''] has the role ['' + g.name + '']. This user can perform tasks beyond just reading and writing data.'') FROM (SELECT memberuid = convert(int, member_principal_id), groupuid = convert(int, role_principal_id) FROM [?].sys.database_role_members) m inner join [?].dbo.sysusers u on m.memberuid = u.uid inner join sysusers g on m.groupuid = g.uid where u.name <> ''dbo'' and g.name in (''db_owner'' , ''db_accessadmin'' , ''db_securityadmin'' , ''db_ddladmin'') OPTION (RECOMPILE);'; END; - /*Check for non-aligned indexes in partioned databases*/ IF NOT EXISTS ( SELECT 1 @@ -5494,7 +7654,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 72) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - insert into #partdb(dbname, objectname, type_desc) + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + insert into #partdb(dbname, objectname, type_desc) SELECT distinct db_name(DB_ID()) as DBName,o.name Object_Name,ds.type_desc FROM sys.objects AS o JOIN sys.indexes AS i ON o.object_id = i.object_id JOIN sys.data_spaces ds on ds.data_space_id = i.data_space_id @@ -5524,27 +7685,27 @@ IF @ProductVersionMajor >= 10 'Performance' AS FindingsGroup , 'The partitioned database ' + dbname + ' may have non-aligned indexes' AS Finding , - 'https://BrentOzar.com/go/aligned' AS URL , + 'https://www.brentozar.com/go/aligned' AS URL , 'Having non-aligned indexes on partitioned tables may cause inefficient query plans and CPU pressure' AS Details FROM #partdb WHERE dbname IS NOT NULL AND dbname NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 72); DROP TABLE #partdb; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 113 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 113) WITH NOWAIT; - + EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5553,11 +7714,11 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT DISTINCT 113, - ''?'', + N''?'', 50, ''Reliability'', ''Full Text Indexes Not Updating'', - ''https://BrentOzar.com/go/fulltext'', + ''https://www.brentozar.com/go/fulltext'', (''At least one full text index in this database has not been crawled in the last week.'') from [?].sys.fulltext_indexes i WHERE change_tracking_state_desc <> ''AUTO'' AND i.is_enabled = 1 AND i.crawl_end_date < DATEADD(dd, -7, GETDATE()) OPTION (RECOMPILE);'; END; @@ -5570,7 +7731,8 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 115) WITH NOWAIT; EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5579,16 +7741,15 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 115, - ''?'', + N''?'', 110, ''Performance'', ''Parallelism Rocket Surgery'', - ''https://BrentOzar.com/go/makeparallel'', + ''https://www.brentozar.com/go/makeparallel'', (''['' + DB_NAME() + ''] has a make_parallel function, indicating that an advanced developer may be manhandling SQL Server into forcing queries to go parallel.'') from [?].INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_NAME = ''make_parallel'' AND ROUTINE_TYPE = ''FUNCTION'' OPTION (RECOMPILE);'; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 122 ) @@ -5596,14 +7757,15 @@ IF @ProductVersionMajor >= 10 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 122) WITH NOWAIT; - /* SQL Server 2012 and newer uses temporary stats for AlwaysOn Availability Groups, and those show up as user-created */ + /* SQL Server 2012 and newer uses temporary stats for Availability Groups, and those show up as user-created */ IF EXISTS (SELECT * FROM sys.all_columns c INNER JOIN sys.all_objects o ON c.object_id = o.object_id WHERE c.name = 'is_temporary' AND o.name = 'stats') EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5612,18 +7774,19 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT TOP 1 122, - ''?'', + N''?'', 200, ''Performance'', ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', + ''https://www.brentozar.com/go/userstats'', (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') from [?].sys.stats WHERE user_created = 1 AND is_temporary = 0 HAVING SUM(1) > 0 OPTION (RECOMPILE);'; ELSE EXEC dbo.sp_MSforeachdb 'USE [?]; - INSERT INTO #BlitzResults + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, @@ -5632,19 +7795,17 @@ IF @ProductVersionMajor >= 10 URL, Details) SELECT 122, - ''?'', + N''?'', 200, ''Performance'', ''User-Created Statistics In Place'', - ''https://BrentOzar.com/go/userstats'', + ''https://www.brentozar.com/go/userstats'', (''['' + DB_NAME() + ''] has '' + CAST(SUM(1) AS NVARCHAR(10)) + '' user-created statistics. This indicates that someone is being a rocket scientist with the stats, and might actually be slowing things down, especially during stats updates.'') from [?].sys.stats WHERE user_created = 1 HAVING SUM(1) > 0 OPTION (RECOMPILE);'; - END; /* IF NOT EXISTS ( SELECT 1 */ - /*Check for high VLF count: this will omit any database snapshots*/ IF NOT EXISTS ( SELECT 1 @@ -5654,11 +7815,12 @@ IF @ProductVersionMajor >= 10 IF @ProductVersionMajor >= 11 BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo2012 + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #LogInfo2012 EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; IF @@ROWCOUNT > 999 BEGIN @@ -5675,7 +7837,7 @@ IF @ProductVersionMajor >= 10 ,170 ,''File Configuration'' ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' + ,''https://www.brentozar.com/go/vlf'' ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' FROM #LogInfo2012 WHERE EXISTS (SELECT name FROM master.sys.databases @@ -5686,11 +7848,12 @@ IF @ProductVersionMajor >= 10 END; ELSE BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] (pre-2012 version of Log Info).', 0, 1, 69) WITH NOWAIT; EXEC sp_MSforeachdb N'USE [?]; - INSERT INTO #LogInfo + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #LogInfo EXEC sp_executesql N''DBCC LogInfo() WITH NO_INFOMSGS''; IF @@ROWCOUNT > 999 BEGIN @@ -5707,7 +7870,7 @@ IF @ProductVersionMajor >= 10 ,170 ,''File Configuration'' ,''High VLF Count'' - ,''https://BrentOzar.com/go/vlf'' + ,''https://www.brentozar.com/go/vlf'' ,''The ['' + DB_NAME() + ''] database has '' + CAST(COUNT(*) as VARCHAR(20)) + '' virtual log files (VLFs). This may be slowing down startup, restores, and even inserts/updates/deletes.'' FROM #LogInfo WHERE EXISTS (SELECT name FROM master.sys.databases @@ -5718,17 +7881,33 @@ IF @ProductVersionMajor >= 10 END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 80 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 80) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://BrentOzar.com/go/maxsize'', (''The ['' + DB_NAME() + ''] database file '' + name + '' has a max file size set to '' + CAST(CAST(max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') FROM sys.database_files WHERE max_size <> 268435456 AND max_size <> -1 AND type <> 2 AND name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; + EXEC dbo.sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT DISTINCT 80, DB_NAME(), 170, ''Reliability'', ''Max File Size Set'', ''https://www.brentozar.com/go/maxsize'', + (''The ['' + DB_NAME() + ''] database file '' + df.name + '' has a max file size set to '' + + CAST(CAST(df.max_size AS BIGINT) * 8 / 1024 AS VARCHAR(100)) + + ''MB. If it runs out of space, the database will stop working even though there may be drive space available.'') + FROM sys.database_files df + WHERE 0 = (SELECT is_read_only FROM sys.databases WHERE name = ''?'') + AND df.max_size <> 268435456 + AND df.max_size <> -1 + AND df.type <> 2 + AND df.growth > 0 + AND df.name <> ''DWDiagnostics'' OPTION (RECOMPILE);'; + + DELETE br + FROM #BlitzResults br + INNER JOIN #SkipChecks sc ON sc.CheckID = 80 AND br.DatabaseName = sc.DatabaseName; END; - + /* Check if columnstore indexes are in use - for Github issue #615 */ IF NOT EXISTS ( SELECT 1 @@ -5736,44 +7915,264 @@ IF @ProductVersionMajor >= 10 WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ BEGIN TRUNCATE TABLE #TemporaryDatabaseResults; - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 74) WITH NOWAIT; - EXEC dbo.sp_MSforeachdb 'USE [?]; IF EXISTS(SELECT * FROM sys.indexes WHERE type IN (5,6)) 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.indexes WHERE type IN (5,6)) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @ColumnStoreIndexesInUse = 1; END; + /* Check if Query Store is in use - for Github issue #3527 */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 74 ) /* Trace flags */ + AND @ProductVersionMajor > 12 /* The relevant column only exists in versions that support Query store */ + BEGIN + TRUNCATE TABLE #TemporaryDatabaseResults; + + 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 AND database_id <> 3) INSERT INTO #TemporaryDatabaseResults (DatabaseName, Finding) VALUES (DB_NAME(), ''Yup'') OPTION (RECOMPILE);'; + IF EXISTS (SELECT * FROM #TemporaryDatabaseResults) SET @QueryStoreInUse = 1; + END; /* Non-Default Database Scoped Config - Github issue #598 */ IF EXISTS ( SELECT * FROM sys.all_objects WHERE [name] = 'database_scoped_configurations' ) BEGIN - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] through [%d].', 0, 1, 194, 197) WITH NOWAIT; + 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; - EXEC dbo.sp_MSforeachdb 'USE [?]; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) - SELECT def1.CheckID, DB_NAME(), 210, ''Non-Default Database Scoped Config'', dsc.[name], ''https://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 + 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', 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), + (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'')) + FROM [?].sys.database_scoped_configurations dsc INNER JOIN #DatabaseScopedConfigurationDefaults def1 ON dsc.configuration_id = def1.configuration_id - LEFT OUTER JOIN #DatabaseScopedConfigurationDefaults def ON dsc.configuration_id = def.configuration_id AND (dsc.value = def.default_value 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 #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);'; - END; + 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 */ + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 218 + ) + BEGIN + IF @Debug IN (1,2) + BEGIN + RAISERROR ('Running CheckId [%d].',0,1,218) WITH NOWAIT; + END + + EXECUTE sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT 218 AS CheckID + ,''?'' AS DatabaseName + ,150 AS Priority + ,''Performance'' AS FindingsGroup + ,''Objects created with dangerous SET Options'' AS Finding + ,''https://www.brentozar.com/go/badset'' AS URL + ,''The '' + QUOTENAME(DB_NAME()) + + '' database has '' + CONVERT(VARCHAR(20),COUNT(1)) + + '' objects that were created with dangerous ANSI_NULL or QUOTED_IDENTIFIER options.'' + + '' These objects can break when using filtered indexes, indexed views'' + + '' and other advanced SQL features.'' AS Details + FROM sys.sql_modules sm + JOIN sys.objects o ON o.[object_id] = sm.[object_id] + AND ( + sm.uses_ansi_nulls <> 1 + OR sm.uses_quoted_identifier <> 1 + ) + AND o.is_ms_shipped = 0 + AND ''?'' != ''rdsadmin'' + HAVING COUNT(1) > 0;'; + END; --of Check 218. + + /* Check 225 - Reliability - Resumable Index Operation Paused */ + IF NOT EXISTS ( + SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL + AND CheckID = 225 + ) + AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'index_resumable_operations') + BEGIN + IF @Debug IN (1,2) + BEGIN + RAISERROR ('Running CheckId [%d].',0,1,218) WITH NOWAIT; + END + + EXECUTE sp_MSforeachdb 'USE [?]; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + SELECT 225 AS CheckID + ,''?'' AS DatabaseName + ,200 AS Priority + ,''Reliability'' AS FindingsGroup + ,''Resumable Index Operation Paused'' AS Finding + ,''https://www.brentozar.com/go/resumable'' AS URL + ,iro.state_desc + N'' since '' + CONVERT(NVARCHAR(50), last_pause_time, 120) + '', '' + + CAST(iro.percent_complete AS NVARCHAR(20)) + ''% complete: '' + + 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 + AND ''?'' != ''rdsadmin'' + ;'; + END; --of Check 225. + + --/* Check 220 - Statistics Without Histograms */ + --IF NOT EXISTS ( + -- SELECT 1 + -- FROM #SkipChecks + -- WHERE DatabaseName IS NULL + -- AND CheckID = 220 + -- ) + -- AND EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_histogram') + --BEGIN + -- IF @Debug IN (1,2) + -- BEGIN + -- RAISERROR ('Running CheckId [%d].',0,1,220) WITH NOWAIT; + -- END + + -- EXECUTE sp_MSforeachdb 'USE [?]; + -- SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + -- INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) + -- SELECT 220 AS CheckID + -- ,DB_NAME() AS DatabaseName + -- ,110 AS Priority + -- ,''Performance'' AS FindingsGroup + -- ,''Statistics Without Histograms'' AS Finding + -- ,''https://www.brentozar.com/go/brokenstats'' AS URL + -- ,CAST(COUNT(DISTINCT o.object_id) AS VARCHAR(100)) + '' tables have statistics that have not been updated since the database was restored or upgraded,'' + -- + '' and have no data in their histogram. See the More Info URL for a script to update them. '' AS Details + -- FROM sys.all_objects o + -- INNER JOIN sys.stats s ON o.object_id = s.object_id AND s.has_filter = 0 + -- OUTER APPLY sys.dm_db_stats_histogram(o.object_id, s.stats_id) h + -- 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'', ''rdsadmin'', ''tempdb'') + -- HAVING COUNT(DISTINCT o.object_id) > 0;'; + --END; --of Check 220. + /*Check for the last good DBCC CHECKDB date */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 68 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; + + /* Removed as populating the #DBCCs table now done in advance as data is uses for multiple checks*/ + --EXEC sp_MSforeachdb N'USE [?]; + --SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + --INSERT #DBCCs + -- (ParentObject, + -- Object, + -- Field, + -- Value) + --EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); + --UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; + + WITH DB2 + AS ( SELECT DISTINCT + Field , + Value , + DbName + FROM #DBCCs + INNER JOIN sys.databases d ON #DBCCs.DbName = d.name + WHERE Field = 'dbi_dbccLastKnownGood' + AND d.create_date < DATEADD(dd, -14, GETDATE()) + ) + INSERT INTO #BlitzResults + ( CheckID , + DatabaseName , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 68 AS CheckID , + DB2.DbName AS DatabaseName , + 1 AS PRIORITY , + 'Reliability' AS FindingsGroup , + 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , + 'https://www.brentozar.com/go/checkdb' AS URL , + 'Last successful CHECKDB: ' + + CASE DB2.Value + WHEN '1900-01-01 00:00:00.000' + THEN ' never.' + ELSE DB2.Value + END AS Details + FROM DB2 + WHERE DB2.DbName <> 'tempdb' + AND DB2.DbName NOT IN ( SELECT DISTINCT + DatabaseName + FROM + #SkipChecks + WHERE CheckID IS NULL OR CheckID = 68) + AND DB2.DbName NOT IN ( SELECT name + FROM sys.databases + WHERE is_read_only = 1) + AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, + -14, + CURRENT_TIMESTAMP); + END; - - END; /* IF @CheckUserDatabaseObjects = 1 */ + END; /* IF @CheckUserDatabaseObjects = 1 */ - IF @CheckProcedureCache = 1 + IF @CheckProcedureCache = 1 BEGIN @@ -5800,7 +8199,7 @@ IF @ProductVersionMajor >= 10 100 AS Priority , 'Performance' AS FindingsGroup , 'Single-Use Plans in Procedure Cache' AS Finding , - 'https://BrentOzar.com/go/single' AS URL , + 'https://www.brentozar.com/go/single' AS URL , ( CAST(COUNT(*) AS VARCHAR(10)) + ' query plans are taking up memory in the procedure cache. This may be wasted memory if we cache plans for queries that never get called again. This may be a good use case for SQL Server 2008''s Optimize for Ad Hoc or for Forced Parameterization.' ) AS Details FROM sys.dm_exec_cached_plans AS cp @@ -5815,7 +8214,6 @@ IF @ProductVersionMajor >= 10 HAVING COUNT(*) > 1; END; - /* Set up the cache tables. Different on 2005 since it doesn't support query_hash, query_plan_hash. */ IF @@VERSION LIKE '%Microsoft SQL Server 2005%' BEGIN @@ -6000,7 +8398,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'Implicit Conversion' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , + 'https://www.brentozar.com/go/implicit' AS URL , ( 'One of the top resource-intensive queries is comparing two fields that are not the same datatype.' ) AS Details , qs.query_plan , qs.query_plan_filtered @@ -6032,7 +8430,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'Implicit Conversion Affecting Cardinality' AS Finding , - 'https://BrentOzar.com/go/implicit' AS URL , + 'https://www.brentozar.com/go/implicit' AS URL , ( 'One of the top resource-intensive queries has an implicit conversion that is affecting cardinality estimation.' ) AS Details , qs.query_plan , qs.query_plan_filtered @@ -6063,7 +8461,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'RID or Key Lookups' AS Finding , - 'https://BrentOzar.com/go/lookup' AS URL , + 'https://www.brentozar.com/go/lookup' AS URL , 'One of the top resource-intensive queries contains RID or Key Lookups. Try to avoid them by creating covering indexes.' AS Details , qs.query_plan , qs.query_plan_filtered @@ -6072,7 +8470,6 @@ IF @ProductVersionMajor >= 10 CAST(qs.query_plan AS NVARCHAR(MAX))) LIKE '%Lookup="1"%'; END; /* @cms4j, 29.11.2013: Look for RID or Key Lookups */ - /* Look for missing indexes */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -6095,7 +8492,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'Missing Index' AS Finding , - 'https://BrentOzar.com/go/missingindex' AS URL , + 'https://www.brentozar.com/go/missingindex' AS URL , ( 'One of the top resource-intensive queries may be dramatically improved by adding an index.' ) AS Details , qs.query_plan , qs.query_plan_filtered @@ -6126,7 +8523,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'Cursor' AS Finding , - 'https://BrentOzar.com/go/cursor' AS URL , + 'https://www.brentozar.com/go/cursor' AS URL , ( 'One of the top resource-intensive queries is using a cursor.' ) AS Details , qs.query_plan , qs.query_plan_filtered @@ -6158,7 +8555,7 @@ IF @ProductVersionMajor >= 10 120 AS Priority , 'Query Plans' AS FindingsGroup , 'Scalar UDFs' AS Finding , - 'https://BrentOzar.com/go/functions' AS URL , + 'https://www.brentozar.com/go/functions' AS URL , ( 'One of the top resource-intensive queries is using a user-defined scalar function that may inhibit parallelism.' ) AS Details , qs.query_plan , qs.query_plan_filtered @@ -6169,7 +8566,7 @@ IF @ProductVersionMajor >= 10 END; /* IF @CheckProcedureCache = 1 */ END; - + /*Check to see if the HA endpoint account is set at the same as the SQL Server Service Account*/ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 @@ -6178,7 +8575,7 @@ IF @ProductVersionMajor >= 10 IF SERVERPROPERTY('IsHadrEnabled') = 1 BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 187) WITH NOWAIT; INSERT INTO [#BlitzResults] @@ -6193,7 +8590,7 @@ IF @ProductVersionMajor >= 10 230 AS [Priority] , 'Security' AS [FindingsGroup] , 'Endpoints Owned by Users' AS [Finding] , - 'https://BrentOzar.com/go/owners' AS [URL] , + 'https://www.brentozar.com/go/owners' AS [URL] , ( 'Endpoint ' + ep.[name] + ' is owned by ' + SUSER_NAME(ep.principal_id) + '. If the endpoint owner login is disabled or not available due to Active Directory problems, the high availability will stop working.' ) AS [Details] FROM sys.database_mirroring_endpoints ep @@ -6201,69 +8598,6 @@ IF @ProductVersionMajor >= 10 WHERE s.service_account IS NULL AND ep.principal_id <> 1; END; - /*Check for the last good DBCC CHECKDB date */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 68 ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 68) WITH NOWAIT; - - EXEC sp_MSforeachdb N'USE [?]; - INSERT #DBCCs - (ParentObject, - Object, - Field, - Value) - EXEC (''DBCC DBInfo() With TableResults, NO_INFOMSGS''); - UPDATE #DBCCs SET DbName = N''?'' WHERE DbName IS NULL OPTION (RECOMPILE);'; - - WITH DB2 - AS ( SELECT DISTINCT - Field , - Value , - DbName - FROM #DBCCs - WHERE Field = 'dbi_dbccLastKnownGood' - ) - INSERT INTO #BlitzResults - ( CheckID , - DatabaseName , - Priority , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT 68 AS CheckID , - DB2.DbName AS DatabaseName , - 1 AS PRIORITY , - 'Reliability' AS FindingsGroup , - 'Last good DBCC CHECKDB over 2 weeks old' AS Finding , - 'https://BrentOzar.com/go/checkdb' AS URL , - 'Last successful CHECKDB: ' - + CASE DB2.Value - WHEN '1900-01-01 00:00:00.000' - THEN ' never.' - ELSE DB2.Value - END AS Details - FROM DB2 - WHERE DB2.DbName <> 'tempdb' - AND DB2.DbName NOT IN ( SELECT DISTINCT - DatabaseName - FROM - #SkipChecks - WHERE CheckID IS NULL OR CheckID = 68) - AND DB2.DbName NOT IN ( SELECT name - FROM sys.databases - WHERE is_read_only = 1) - AND CONVERT(DATETIME, DB2.Value, 121) < DATEADD(DD, - -14, - CURRENT_TIMESTAMP); - END; - - - /*Verify that the servername is set */ IF NOT EXISTS ( SELECT 1 @@ -6287,7 +8621,7 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Informational' AS FindingsGroup , '@@Servername Not Set' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , + 'https://www.brentozar.com/go/servername' AS URL , '@@Servername variable is null. You can fix it by executing: "sp_addserver '''', local"' AS Details; END; @@ -6295,7 +8629,7 @@ IF @ProductVersionMajor >= 10 (@@SERVERNAME IS NOT NULL AND /* not a named instance */ - CHARINDEX('\',CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 + CHARINDEX(CHAR(92),CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))) = 0 AND /* not clustered, when computername may be different than the servername */ SERVERPROPERTY('IsClustered') = 0 @@ -6318,7 +8652,7 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Configuration' AS FindingsGroup , '@@Servername Not Correct' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , + 'https://www.brentozar.com/go/servername' AS URL , 'The @@Servername is different than the computer name, which may trigger certificate errors.' AS Details; END; @@ -6330,21 +8664,10 @@ IF @ProductVersionMajor >= 10 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 73) WITH NOWAIT; + + INSERT INTO #AlertInfo + EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; - DECLARE @AlertInfo TABLE - ( - FailSafeOperator NVARCHAR(255) , - NotificationMethod INT , - ForwardingServer NVARCHAR(255) , - ForwardingSeverity INT , - PagerToTemplate NVARCHAR(255) , - PagerCCTemplate NVARCHAR(255) , - PagerSubjectTemplate NVARCHAR(255) , - PagerSendSubjectOnly NVARCHAR(255) , - ForwardAlways INT - ); - INSERT INTO @AlertInfo - EXEC [master].[dbo].[sp_MSgetalertinfo] @includeaddresses = 0; INSERT INTO #BlitzResults ( CheckID , Priority , @@ -6356,14 +8679,14 @@ IF @ProductVersionMajor >= 10 SELECT 73 AS CheckID , 200 AS Priority , 'Monitoring' AS FindingsGroup , - 'No failsafe operator configured' AS Finding , - 'https://BrentOzar.com/go/failsafe' AS URL , + 'No Failsafe Operator Configured' AS Finding , + 'https://www.brentozar.com/go/failsafe' AS URL , ( 'No failsafe operator is configured on this server. This is a good idea just in-case there are issues with the [msdb] database that prevents alerting.' ) AS Details - FROM @AlertInfo + FROM #AlertInfo WHERE FailSafeOperator IS NULL; END; -/*Identify globally enabled trace flags*/ + /*Identify globally enabled trace flags*/ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 74 ) @@ -6385,26 +8708,132 @@ IF @ProductVersionMajor >= 10 SELECT 74 AS CheckID , 200 AS Priority , 'Informational' AS FindingsGroup , - 'TraceFlag On' AS Finding , + 'Trace Flag On' AS Finding , CASE WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN 'https://support.microsoft.com/en-us/kb/3210239' + WHEN [T].[TraceFlag] IN ('7745', '7752') THEN 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' ELSE'https://www.BrentOzar.com/go/traceflags/' END AS URL , - 'Trace flag ' + - CASE WHEN [T].[TraceFlag] = '2330' THEN ' 2330 enabled globally. Using this trace Flag disables missing index requests' - WHEN [T].[TraceFlag] = '1211' THEN ' 1211 enabled globally. Using this Trace Flag disables lock escalation when you least expect it. No Bueno!' - WHEN [T].[TraceFlag] = '1224' THEN ' 1224 enabled globally. Using this Trace Flag disables lock escalation based on the number of locks being taken. You shouldn''t have done that, Dave.' - WHEN [T].[TraceFlag] = '652' THEN ' 652 enabled globally. Using this Trace Flag disables pre-fetching during index scans. If you hate slow queries, you should turn that off.' - WHEN [T].[TraceFlag] = '661' THEN ' 661 enabled globally. Using this Trace Flag disables ghost record removal. Who you gonna call? No one, turn that thing off.' - WHEN [T].[TraceFlag] = '1806' THEN ' 1806 enabled globally. Using this Trace Flag disables instant file initialization. I question your sanity.' - WHEN [T].[TraceFlag] = '3505' THEN ' 3505 enabled globally. Using this Trace Flag disables Checkpoints. Probably not the wisest idea.' - WHEN [T].[TraceFlag] = '8649' THEN ' 8649 enabled globally. Using this Trace Flag drops cost thresholf for parallelism down to 0. I hope this is a dev server.' - WHEN [T].[TraceFlag] = '834' AND @ColumnStoreIndexesInUse = 1 THEN ' 834 is enabled globally. Using this Trace Flag with Columnstore Indexes is not a great idea.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN ' 8017 is enabled globally, which is the default for express edition.' - WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN ' 8017 is enabled globally. Using this Trace Flag disables creation schedulers for all logical processors. Not good.' - ELSE [T].[TraceFlag] + ' is enabled globally.' END + 'Trace flag ' + + 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.' + WHEN [T].[TraceFlag] = '1204' THEN '1204 enabled globally, which captures deadlock graphs in the error log.' + WHEN [T].[TraceFlag] = '1222' THEN '1222 enabled globally, which captures deadlock graphs in the error log.' + WHEN [T].[TraceFlag] = '1224' THEN '1224 enabled globally, which disables lock escalation until the server has memory pressure. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '1806' THEN '1806 enabled globally, which disables Instant File Initialization, causing restores and file growths to take longer. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '2330' THEN '2330 enabled globally, which disables missing index requests. This is usually a very bad idea.' + WHEN [T].[TraceFlag] = '2371' THEN '2371 enabled globally, which changes the auto update stats threshold.' + WHEN [T].[TraceFlag] = '3023' THEN '3023 enabled globally, which performs checksums by default when doing database backups.' + 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?' + WHEN [T].[TraceFlag] = '8048' THEN '8048 enabled globally, which tries to reduce CMEMTHREAD waits on servers with a lot of logical processors.' + WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) LIKE N'%Express%') THEN '8017 is enabled globally, but this is the default for Express Edition.' + WHEN [T].[TraceFlag] = '8017' AND (CAST(SERVERPROPERTY('Edition') AS NVARCHAR(1000)) NOT LIKE N'%Express%') THEN '8017 is enabled globally, which disables the creation of schedulers for all logical processors.' + WHEN [T].[TraceFlag] = '8649' THEN '8649 enabled globally, which cost threshold for parallelism down to 0. This is usually a very bad idea.' + ELSE [T].[TraceFlag] + ' is enabled globally.' END AS Details FROM #TraceStatus T; + + + IF NOT EXISTS ( SELECT 1 + FROM #TraceStatus T + WHERE [T].[TraceFlag] = '7745' ) + AND @QueryStoreInUse = 1 + + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + '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; + END; + + IF NOT EXISTS ( SELECT 1 + FROM #TraceStatus T + WHERE [T].[TraceFlag] = '7752' ) + AND @ProductVersionMajor < 15 + AND @QueryStoreInUse = 1 + + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 74 AS CheckID , + 200 AS Priority , + 'Informational' AS FindingsGroup , + 'Recommended Trace Flag Off' AS Finding , + 'https://www.sqlskills.com/blogs/erin/query-store-trace-flags/' AS URL , + 'Trace Flag 7752 not enabled globally. It stops queries needing to wait on Query Store loading up after database recovery. It is so recommended that it is enabled by default as of SQL Server 2019.' AS Details + FROM #TraceStatus T + END; END; + /* High CMEMTHREAD waits that could need trace flag 8048. + This check has to be run AFTER the globally enabled trace flag check, + since it uses the #TraceStatus table to know if flags are enabled. + */ + IF @ProductVersionMajor >= 11 AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 162 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 162) WITH NOWAIT; + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 162 AS CheckID , + 50 AS Priority , + 'Performance' AS FindingGroup , + 'Poison Wait Detected: CMEMTHREAD and NUMA' AS Finding , + 'https://www.brentozar.com/go/poison' AS URL , + CONVERT(VARCHAR(10), (MAX([wait_time_ms]) / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (MAX([wait_time_ms]) / 1000), 0), 108) + ' of this wait have been recorded' + + CASE WHEN ts.status = 1 THEN ' despite enabling trace flag 8048 already.' + ELSE '. In servers with over 8 cores per NUMA node, when CMEMTHREAD waits are a bottleneck, trace flag 8048 may be needed.' + END + FROM sys.dm_os_nodes n + INNER JOIN sys.[dm_os_wait_stats] w ON w.wait_type = 'CMEMTHREAD' + LEFT OUTER JOIN #TraceStatus ts ON ts.TraceFlag = 8048 AND ts.status = 1 + WHERE n.node_id = 0 AND n.online_scheduler_count >= 8 + AND EXISTS (SELECT * FROM sys.dm_os_nodes WHERE node_id > 0 AND node_state_desc NOT LIKE '%DAC') + GROUP BY w.wait_type, ts.status + HAVING SUM([wait_time_ms]) > (SELECT 5000 * datediff(HH,create_date,CURRENT_TIMESTAMP) AS hours_since_startup FROM sys.databases WHERE name='tempdb') + AND SUM([wait_time_ms]) > 60000; + END; + + /*Check for transaction log file larger than data file */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -6427,7 +8856,7 @@ IF @ProductVersionMajor >= 10 50 AS Priority , 'Reliability' AS FindingsGroup , 'Transaction Log Larger than Data File' AS Finding , - 'https://BrentOzar.com/go/biglog' AS URL , + 'https://www.brentozar.com/go/biglog' AS URL , 'The database [' + DB_NAME(a.database_id) + '] has a ' + CAST((CAST(a.size AS BIGINT) * 8 / 1000000) AS NVARCHAR(20)) + ' GB transaction log file, larger than the total data file sizes. This may indicate that transaction log backups are not being performed or not performed often enough.' AS Details FROM sys.master_files a @@ -6435,7 +8864,7 @@ IF @ProductVersionMajor >= 10 AND DB_NAME(a.database_id) NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID = 75 OR CheckID IS NULL) AND a.size > 125000 /* Size is measured in pages here, so this gets us log files over 1GB. */ AND a.size > ( SELECT SUM(CAST(b.size AS BIGINT)) @@ -6471,14 +8900,14 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Informational' AS FindingsGroup , 'Collation is ' + collation_name AS Finding , - 'https://BrentOzar.com/go/collate' AS URL , + 'https://www.brentozar.com/go/collate' AS URL , 'Collation differences between user databases and tempdb can cause conflicts especially when comparing string values' AS Details FROM sys.databases WHERE name NOT IN ( 'master', 'model', 'msdb') AND name NOT LIKE 'ReportServer%' AND name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID IS NULL OR CheckID = 76) AND collation_name <> ( SELECT collation_name @@ -6507,10 +8936,10 @@ IF @ProductVersionMajor >= 10 ) SELECT 77 AS CheckID , dSnap.[name] AS DatabaseName , - 50 AS Priority , + 170 AS Priority , 'Reliability' AS FindingsGroup , 'Database Snapshot Online' AS Finding , - 'https://BrentOzar.com/go/snapshot' AS URL , + 'https://www.brentozar.com/go/snapshot' AS URL , 'Database [' + dSnap.[name] + '] is a snapshot of [' + dOriginal.[name] @@ -6519,7 +8948,7 @@ IF @ProductVersionMajor >= 10 INNER JOIN sys.databases dOriginal ON dSnap.source_database_id = dOriginal.database_id AND dSnap.name NOT IN ( SELECT DISTINCT DatabaseName - FROM #SkipChecks + FROM #SkipChecks WHERE CheckID = 77 OR CheckID IS NULL); END; @@ -6548,20 +8977,20 @@ IF @ProductVersionMajor >= 10 END AS Priority, 'Performance' AS FindingsGroup , 'Shrink Database Job' AS Finding , - 'https://BrentOzar.com/go/autoshrink' AS URL , + 'https://www.brentozar.com/go/autoshrink' AS URL , 'In the [' + j.[name] + '] job, step [' + step.[step_name] + '] has SHRINKDATABASE or SHRINKFILE, which may be causing database fragmentation.' + CASE WHEN COALESCE(ssc.name,'0') != '0' THEN + ' (Schedule: [' + ssc.name + '])' ELSE + '' END AS Details FROM msdb.dbo.sysjobs j INNER JOIN msdb.dbo.sysjobsteps step ON j.job_id = step.job_id - LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc + LEFT OUTER JOIN msdb.dbo.sysjobschedules AS sjsc ON j.job_id = sjsc.job_id - LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc + LEFT OUTER JOIN msdb.dbo.sysschedules AS ssc ON sjsc.schedule_id = ssc.schedule_id AND sjsc.job_id = j.job_id - LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh - ON j.job_id = sjh.job_id + LEFT OUTER JOIN msdb.dbo.sysjobhistory AS sjh + ON j.job_id = sjh.job_id AND step.step_id = sjh.step_id AND sjh.run_date IN (SELECT max(sjh2.run_date) FROM msdb.dbo.sysjobhistory AS sjh2 WHERE sjh2.job_id = j.job_id) -- get the latest entry date AND sjh.run_time IN (SELECT max(sjh3.run_time) FROM msdb.dbo.sysjobhistory AS sjh3 WHERE sjh3.job_id = j.job_id AND sjh3.run_date = sjh.run_date) -- get the latest entry time @@ -6618,14 +9047,13 @@ IF @ProductVersionMajor >= 10 200 AS Priority , 'Informational' AS FindingsGroup , 'Agent Jobs Starting Simultaneously' AS Finding , - 'https://BrentOzar.com/go/busyagent/' AS URL , + 'https://www.brentozar.com/go/busyagent/' AS URL , ( 'Multiple SQL Server Agent jobs are configured to start simultaneously. For detailed schedule listings, see the query in the URL.' ) AS Details FROM msdb.dbo.sysjobactivity WHERE start_execution_date > DATEADD(dd, -14, GETDATE()) GROUP BY start_execution_date HAVING COUNT(*) > 1; END; - IF @CheckServerInfo = 1 BEGIN @@ -6642,7 +9070,7 @@ IF @ProductVersionMajor >= 10 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -6661,14 +9089,10 @@ IF @ProductVersionMajor >= 10 ELSE 'https://en.wikipedia.org/wiki/List_of_Linux_distributions' END ) AS [URL] , - ( CASE + ( CASE WHEN [ohi].[host_platform] = 'Linux' THEN 'You''re running the ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ' distribution of ' + CAST([ohi].[host_platform] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' AND [ohi].[host_release] < '6' THEN 'You''re running a really old version: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] >= '6' AND [ohi].[host_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.2' THEN 'You''re running a rather modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) - WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '6.3' THEN 'Hot dog! You''re living in the future! You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] = '5' THEN 'You''re running Windows 2000, version ' + CAST([ohi].[host_release] AS VARCHAR(5)) + WHEN [ohi].[host_platform] = 'Windows' AND [ohi].[host_release] > '5' THEN 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(50)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) ELSE 'You''re running ' + CAST([ohi].[host_distribution] AS VARCHAR(35)) + ', version ' + CAST([ohi].[host_release] AS VARCHAR(5)) END ) AS [Details] @@ -6683,9 +9107,9 @@ IF @ProductVersionMajor >= 10 WHERE name = 'dm_os_windows_info' ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 172) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -6700,14 +9124,13 @@ IF @ProductVersionMajor >= 10 'Server Info' AS [FindingsGroup] , 'Windows Version' AS [Finding] , 'https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions' AS [URL] , - ( CASE - WHEN [owi].[windows_release] = '5' THEN 'You''re running a really old version: Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running a really old version: Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running a pretty old version: Windows: Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.2' THEN 'You''re running a rather modern version of Windows: Server 2012 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '6.3' THEN 'You''re running a pretty modern version of Windows: Server 2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - WHEN [owi].[windows_release] = '10.0' THEN 'You''re running a pretty modern version of Windows: Server 2016 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) - ELSE 'Hot dog! You''re living in the future! You''re running version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + ( CASE + WHEN [owi].[windows_release] = '5' THEN 'You''re running Windows 2000, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] > '5' AND [owi].[windows_release] < '6' THEN 'You''re running Windows Server 2003/2003R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] >= '6' AND [owi].[windows_release] <= '6.1' THEN 'You''re running Windows Server 2008/2008R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] >= '6.2' AND [owi].[windows_release] <= '6.3' THEN 'You''re running Windows Server 2012/2012R2 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + WHEN [owi].[windows_release] = '10.0' THEN 'You''re running Windows Server 2016/2019 era, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) + ELSE 'You''re running Windows Server, version ' + CAST([owi].[windows_release] AS VARCHAR(5)) END ) AS [Details] FROM [sys].[dm_os_windows_info] [owi]; @@ -6725,9 +9148,9 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 166 ) BEGIN - + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 166) WITH NOWAIT; - + INSERT INTO [#BlitzResults] ( [CheckID] , [Priority] , @@ -6740,19 +9163,19 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 250 AS [Priority] , 'Server Info' AS [FindingsGroup] , 'Locked Pages In Memory Enabled' AS [Finding] , - 'https://BrentOzar.com/go/lpim' AS [URL] , + 'https://www.brentozar.com/go/lpim' AS [URL] , ( 'You currently have ' + CASE WHEN [dopm].[locked_page_allocations_kb] / 1024. / 1024. > 0 - THEN CAST([dopm].[locked_page_allocations_kb] / 1024. / 1024. AS VARCHAR(100)) + THEN CAST([dopm].[locked_page_allocations_kb] / 1024 / 1024 AS VARCHAR(100)) + ' GB' - ELSE CAST([dopm].[locked_page_allocations_kb] / 1024. AS VARCHAR(100)) + ELSE CAST([dopm].[locked_page_allocations_kb] / 1024 AS VARCHAR(100)) + ' MB' END + ' of pages locked in memory.' ) AS [Details] FROM [sys].[dm_os_process_memory] AS [dopm] WHERE [dopm].[locked_page_allocations_kb] > 0; - END; + END; /* Server Info - Locked Pages In Memory Enabled - Check 166 - SQL Server 2016 SP1 and newer */ IF NOT EXISTS ( SELECT 1 @@ -6772,7 +9195,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 250 AS Priority , ''Server Info'' AS FindingsGroup , ''Memory Model Unconventional'' AS Finding , - ''https://BrentOzar.com/go/lpim'' AS URL , + ''https://www.brentozar.com/go/lpim'' AS URL , ''Memory Model: '' + CAST(sql_memory_model_desc AS NVARCHAR(100)) FROM sys.dm_os_sys_info WHERE sql_memory_model <> 1 OPTION (RECOMPILE);'; @@ -6782,72 +9205,147 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 EXECUTE(@StringToExecute); END; - - - /* - Starting with SQL Server 2014 SP2, Instant File Initialization - is logged in the SQL Server Error Log. - */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 184 ) - AND (@ProductVersionMajor >= 13) OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000) + /* Performance - Instant File Initialization Not Enabled - Check 192 */ + /* Server Info - Instant File Initialization Enabled - Check 193 */ + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 192 /* IFI disabled check disabled */ + ) OR NOT EXISTS + ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 193 /* IFI enabled check disabled */ + ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d] and CheckId [%d].', 0, 1, 192, 193) WITH NOWAIT; + + DECLARE @IFISetting varchar(1) = N'N' + ,@IFIReadDMVFailed bit = 0 + ,@IFIAllFailed bit = 0; + + /* See if we can get the instant_file_initialization_enabled column from sys.dm_server_services */ + IF EXISTS + ( + SELECT 1/0 + FROM sys.all_columns + WHERE [object_id] = OBJECT_ID(N'[sys].[dm_server_services]') + AND [name] = N'instant_file_initialization_enabled' + ) BEGIN + /* This needs to be a "dynamic" SQL statement because if the 'instant_file_initialization_enabled' column doesn't exist the procedure might fail on a bind error */ + SET @StringToExecute = N'SELECT @IFISetting = instant_file_initialization_enabled' + @crlf + + N'FROM sys.dm_server_services' + @crlf + + N'WHERE filename LIKE ''%sqlservr.exe%''' + @crlf + + N'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.'; + + EXEC dbo.sp_executesql + @StringToExecute + ,N'@IFISetting varchar(1) OUTPUT' + ,@IFISetting = @IFISetting OUTPUT - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 184) WITH NOWAIT; - - INSERT INTO #ErrorLog - EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; - - IF @@ROWCOUNT > 0 - INSERT INTO #BlitzResults - ( CheckID , - [Priority] , - FindingsGroup , - Finding , - URL , - Details - ) - SELECT - 193 AS [CheckID] , - 250 AS [Priority] , - 'Server Info' AS [FindingsGroup] , - 'Instant File Initialization Enabled' AS [Finding] , - 'https://BrentOzar.com/go/instant' AS [URL] , - 'The service account has the Perform Volume Maintenance Tasks permission.'; - END; - - /* Server Info - Instant File Initialization Not Enabled - Check 192 - SQL Server 2016 SP1 and newer */ - IF NOT EXISTS ( SELECT 1 - FROM #SkipChecks - WHERE DatabaseName IS NULL AND CheckID = 192 ) - AND EXISTS ( SELECT * - FROM sys.all_objects o - INNER JOIN sys.all_columns c ON o.object_id = c.object_id - WHERE o.name = 'dm_server_services' - AND c.name = 'instant_file_initialization_enabled' ) - BEGIN - - IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 192) WITH NOWAIT; - - SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - SELECT 192 AS CheckID , - 50 AS Priority , - ''Server Info'' AS FindingsGroup , - ''Instant File Initialization Not Enabled'' AS Finding , - ''https://BrentOzar.com/go/instant'' AS URL , - ''Consider enabling IFI for faster restores and data file growths.'' - FROM sys.dm_server_services WHERE instant_file_initialization_enabled <> ''Y'' AND filename LIKE ''%sqlservr.exe%'' 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.'; - - EXECUTE(@StringToExecute); - END; - - - + SET @IFIReadDMVFailed = 0; + END + ELSE + /* We couldn't get the instant_file_initialization_enabled column from sys.dm_server_services, fall back to read error log */ + BEGIN + SET @IFIReadDMVFailed = 1; + /* If this is Amazon RDS, we'll use the rdsadmin.dbo.rds_read_error_log */ + IF LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS VARCHAR(8000)), 8) = 'EC2AMAZ-' + AND db_id('rdsadmin') IS NOT NULL + AND EXISTS ( SELECT 1/0 + FROM master.sys.all_objects + WHERE name IN ('rds_startup_tasks', 'rds_help_revlogin', 'rds_hexadecimal', 'rds_failover_tracking', 'rds_database_tracking', 'rds_track_change') + ) + BEGIN + /* Amazon RDS detected, read rdsadmin.dbo.rds_read_error_log */ + INSERT INTO #ErrorLog + EXEC rdsadmin.dbo.rds_read_error_log 0, 1, N'Database Instant File Initialization: enabled'; + END + ELSE + BEGIN + /* Try to read the error log, this might fail due to permissions */ + BEGIN TRY + INSERT INTO #ErrorLog + EXEC sys.xp_readerrorlog 0, 1, N'Database Instant File Initialization: enabled'; + END TRY + BEGIN CATCH + IF @Debug IN (1, 2) RAISERROR('No permissions to execute xp_readerrorlog.', 0, 1) WITH NOWAIT; + SET @IFIAllFailed = 1; + END CATCH + END; + END; + + IF @IFIAllFailed = 0 + BEGIN + IF @IFIReadDMVFailed = 1 + /* We couldn't read the DMV so set the @IFISetting variable using the error log */ + BEGIN + IF EXISTS ( SELECT 1/0 + FROM #ErrorLog + WHERE LEFT([Text], 45) = N'Database Instant File Initialization: enabled' + ) + BEGIN + SET @IFISetting = 'Y'; + END + ELSE + BEGIN + SET @IFISetting = 'N'; + END; + END; + + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 192 /* IFI disabled check disabled */ + ) AND @IFISetting = 'N' + BEGIN + INSERT INTO #BlitzResults + ( + CheckID , + [Priority] , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 192 AS [CheckID] , + 50 AS [Priority] , + 'Performance' AS [FindingsGroup] , + 'Instant File Initialization Not Enabled' AS [Finding] , + 'https://www.brentozar.com/go/instant' AS [URL] , + 'Consider enabling IFI for faster restores and data file growths.' AS [Details] + END; + + IF NOT EXISTS ( SELECT 1/0 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 193 /* IFI enabled check disabled */ + ) AND @IFISetting = 'Y' + BEGIN + INSERT INTO #BlitzResults + ( + CheckID , + [Priority] , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 193 AS [CheckID] , + 250 AS [Priority] , + 'Server Info' AS [FindingsGroup] , + 'Instant File Initialization Enabled' AS [Finding] , + 'https://www.brentozar.com/go/instant' AS [URL] , + 'The service account has the Perform Volume Maintenance Tasks permission.' AS [Details] + END; + END; + END; + /* End of checkId 192 */ + /* End of checkId 193 */ IF NOT EXISTS ( SELECT 1 FROM #SkipChecks @@ -6868,13 +9366,11 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 250 AS Priority , 'Server Info' AS FindingsGroup , 'Server Name' AS Finding , - 'https://BrentOzar.com/go/servername' AS URL , + 'https://www.brentozar.com/go/servername' AS URL , @@SERVERNAME AS Details WHERE @@SERVERNAME IS NOT NULL; END; - - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 83 ) @@ -6894,7 +9390,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; @@ -6959,7 +9455,6 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 END; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 85 ) @@ -6984,17 +9479,21 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + CAST(SERVERPROPERTY('productversion') AS NVARCHAR(100)) + N'. Patch Level: ' + CAST(SERVERPROPERTY('productlevel') AS NVARCHAR(100)) + + CASE WHEN SERVERPROPERTY('ProductUpdateLevel') IS NULL + THEN N'' + ELSE N'. Cumulative Update: ' + + CAST(SERVERPROPERTY('ProductUpdateLevel') AS NVARCHAR(100)) + END + N'. Edition: ' + CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) - + N'. AlwaysOn Enabled: ' + + N'. Availability Groups Enabled: ' + CAST(COALESCE(SERVERPROPERTY('IsHadrEnabled'), 0) AS VARCHAR(100)) - + N'. AlwaysOn Mgr Status: ' + + N'. Availability Groups Manager Status: ' + CAST(COALESCE(SERVERPROPERTY('HadrManagerStatus'), 0) AS VARCHAR(100)); END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 88 ) @@ -7044,7 +9543,6 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 FROM sys.dm_os_sys_info; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 92 ) @@ -7053,10 +9551,40 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 92) WITH NOWAIT; INSERT INTO #driveInfo - ( drive, SIZE ) + ( drive, available_MB ) EXEC master..xp_fixeddrives; - INSERT INTO #BlitzResults + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_os_volume_stats') + BEGIN + SET @StringToExecute = 'Update #driveInfo + SET + logical_volume_name = v.logical_volume_name, + total_MB = v.total_MB, + used_percent = v.used_percent + FROM + #driveInfo + inner join ( + SELECT DISTINCT + SUBSTRING(volume_mount_point, 1, 1) AS volume_mount_point + ,CASE WHEN ISNULL(logical_volume_name,'''') = '''' THEN '''' ELSE ''('' + logical_volume_name + '')'' END AS logical_volume_name + ,total_bytes/1024/1024 AS total_MB + ,available_bytes/1024/1024 AS available_MB + ,(CONVERT(DECIMAL(5,2),(total_bytes/1.0 - available_bytes)/total_bytes * 100)) AS used_percent + FROM + (SELECT TOP 1 WITH TIES + database_id + ,file_id + ,SUBSTRING(physical_name,1,1) AS Drive + FROM sys.master_files + ORDER BY ROW_NUMBER() OVER(PARTITION BY SUBSTRING(physical_name,1,1) ORDER BY database_id) + ) f + CROSS APPLY + sys.dm_os_volume_stats(f.database_id, f.file_id) + ) as v on #driveInfo.drive = v.volume_mount_point;'; + EXECUTE(@StringToExecute); + END; + + SET @StringToExecute ='INSERT INTO #BlitzResults ( CheckID , Priority , FindingsGroup , @@ -7066,17 +9594,32 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 ) SELECT 92 AS CheckID , 250 AS Priority , - 'Server Info' AS FindingsGroup , - 'Drive ' + i.drive + ' Space' AS Finding , - '' AS URL , - CAST(i.SIZE AS VARCHAR(30)) - + 'MB free on ' + i.drive - + ' drive' AS Details - FROM #driveInfo AS i; + ''Server Info'' AS FindingsGroup , + ''Drive '' + i.drive + '' Space'' AS Finding , + '''' AS URL , + CASE WHEN i.total_MB IS NULL THEN + CAST(CAST(i.available_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB free on '' + i.drive + + '' drive'' + ELSE CAST(CAST(i.available_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB free on '' + i.drive + + '' drive '' + i.logical_volume_name + + '' out of '' + CAST(CAST(i.total_MB/1024 AS NUMERIC(18,2)) AS VARCHAR(30)) + + '' GB total ('' + CAST(i.used_percent AS VARCHAR(30)) + ''% used)'' END + AS Details + FROM #driveInfo AS i;' + + IF (@ProductVersionMajor >= 11) + BEGIN + SET @StringToExecute=REPLACE(@StringToExecute,'CAST(i.available_MB/1024 AS NUMERIC(18,2))','FORMAT(i.available_MB/1024,''N2'')'); + SET @StringToExecute=REPLACE(@StringToExecute,'CAST(i.total_MB/1024 AS NUMERIC(18,2))','FORMAT(i.total_MB/1024,''N2'')'); + END; + + EXECUTE(@StringToExecute); + DROP TABLE #driveInfo; END; - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 103 ) @@ -7094,7 +9637,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 250 AS Priority, ''Server Info'' AS FindingsGroup, ''Virtual Server'' AS Finding, - ''https://BrentOzar.com/go/virtual'' AS URL, + ''https://www.brentozar.com/go/virtual'' AS URL, ''Type: ('' + virtual_machine_type_desc + '')'' AS Details FROM sys.dm_os_sys_info WHERE virtual_machine_type <> 0 OPTION (RECOMPILE);'; @@ -7105,6 +9648,34 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 EXECUTE(@StringToExecute); END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 214 ) + AND EXISTS ( SELECT * + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_sys_info' + AND c.name = 'container_type_desc' ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 214) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 214 AS CheckID, + 250 AS Priority, + ''Server Info'' AS FindingsGroup, + ''Container'' AS Finding, + ''https://www.brentozar.com/go/virtual'' AS URL, + ''Type: ('' + container_type_desc + '')'' AS Details + FROM sys.dm_os_sys_info + WHERE container_type_desc <> ''NONE'' 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.'; + + EXECUTE(@StringToExecute); + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 114 ) @@ -7131,10 +9702,10 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + '' Memory node: '' + CAST(n.memory_node_id AS NVARCHAR(10)) + '' Memory VAS Reserved GB: '' + CAST(CAST((m.virtual_address_space_reserved_kb / 1024.0 / 1024) AS INT) AS NVARCHAR(100)) FROM sys.dm_os_nodes n INNER JOIN sys.dm_os_memory_nodes m ON n.memory_node_id = m.memory_node_id - OUTER APPLY (SELECT + OUTER APPLY (SELECT COUNT(*) AS [offline_schedulers] FROM sys.dm_os_schedulers dos - WHERE n.node_id = dos.parent_node_id + WHERE n.node_id = dos.parent_node_id AND dos.status = ''VISIBLE OFFLINE'' ) oac WHERE n.node_state_desc NOT LIKE ''%DAC%'' @@ -7145,13 +9716,120 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 EXECUTE(@StringToExecute); END; + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 211 ) + BEGIN + + /* Variables for check 211: */ + DECLARE + @powerScheme varchar(36) + ,@cpu_speed_mhz int + ,@cpu_speed_ghz decimal(18,2) + ,@ExecResult int; + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 211) WITH NOWAIT; + IF @sa = 0 RAISERROR('The errors: ''xp_regread() returned error 5, ''Access is denied.'''' can be safely ignored', 0, 1) WITH NOWAIT; + + /* Get power plan if set by group policy [Git Hub Issue #1620] */ + EXEC xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'SOFTWARE\Policies\Microsoft\Power\PowerSettings', + @value_name = N'ActivePowerScheme', + @value = @powerScheme OUTPUT, + @no_output = N'no_output'; + + IF @powerScheme IS NULL /* If power plan was not set by group policy, get local value [Git Hub Issue #1620]*/ + EXEC xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes', + @value_name = N'ActivePowerScheme', + @value = @powerScheme OUTPUT; + + /* Get the cpu speed*/ + EXEC @ExecResult = xp_regread @rootkey = N'HKEY_LOCAL_MACHINE', + @key = N'HARDWARE\DESCRIPTION\System\CentralProcessor\0', + @value_name = N'~MHz', + @value = @cpu_speed_mhz OUTPUT; + + /* Convert the Megahertz to Gigahertz */ + IF @ExecResult != 0 RAISERROR('We couldn''t retrieve the CPU speed, you will see Unknown in the results', 0, 1) + + SET @cpu_speed_ghz = CAST(CAST(@cpu_speed_mhz AS decimal) / 1000 AS decimal(18,2)); + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 211 AS CheckId, + 250 AS Priority, + 'Server Info' AS FindingsGroup, + 'Power Plan' AS Finding, + 'https://www.brentozar.com/blitz/power-mode/' AS URL, + 'Your server has ' + + ISNULL(CAST(@cpu_speed_ghz as VARCHAR(8)), 'Unknown ') + + 'GHz CPUs, and is in ' + + CASE @powerScheme + WHEN 'a1841308-3541-4fab-bc81-f71556f20b4a' + THEN 'power saving mode -- are you sure this is a production SQL Server?' + WHEN '381b4222-f694-41f0-9685-ff5bb260df2e' + THEN 'balanced power mode -- Uh... you want your CPUs to run at full speed, right?' + WHEN '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' + THEN 'high performance power mode' + WHEN 'e9a42b02-d5df-448d-aa00-03f14749eb61' + THEN 'ultimate performance power mode' + ELSE 'an unknown power mode.' + END AS Details + + END; + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 212 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 212) WITH NOWAIT; + + INSERT INTO #Instances (Instance_Number, Instance_Name, Data_Field) + EXEC master.sys.xp_regread @rootkey = 'HKEY_LOCAL_MACHINE', + @key = 'SOFTWARE\Microsoft\Microsoft SQL Server', + @value_name = 'InstalledInstances' + + IF (SELECT COUNT(*) FROM #Instances) > 1 + BEGIN + + DECLARE @InstanceCount NVARCHAR(MAX) + SELECT @InstanceCount = COUNT(*) FROM #Instances + INSERT INTO #BlitzResults + ( + CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 212 AS CheckId , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Instance Stacking' AS Finding , + 'https://www.brentozar.com/go/babygotstacked/' AS URL , + 'Your Server has ' + @InstanceCount + ' Instances of SQL Server installed. More than one is usually a bad idea. Read the URL for more info.' + END; + END; + IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 106 ) AND (select convert(int,value_in_use) from sys.configurations where name = 'default trace enabled' ) = 1 AND DATALENGTH( COALESCE( @base_tracefilename, '' ) ) > DATALENGTH('.TRC') + AND @TraceFileIssue = 0 BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 106) WITH NOWAIT; @@ -7169,7 +9847,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 ,250 AS Priority ,'Server Info' AS FindingsGroup ,'Default Trace Contents' AS Finding - ,'https://BrentOzar.com/go/trace' AS URL + ,'https://www.brentozar.com/go/trace' AS URL ,'The default trace holds '+cast(DATEDIFF(hour,MIN(StartTime),GETDATE())as VARCHAR(30))+' hours of data' +' between '+cast(Min(StartTime) as VARCHAR(30))+' and '+cast(GETDATE()as VARCHAR(30)) +('. The default trace files are located in: '+left( @curr_tracefilename,len(@curr_tracefilename) - @indx) @@ -7178,14 +9856,13 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 WHERE EventClass BETWEEN 65500 and 65600; END; /* CheckID 106 */ - IF NOT EXISTS ( SELECT 1 FROM #SkipChecks WHERE DatabaseName IS NULL AND CheckID = 152 ) BEGIN IF EXISTS (SELECT * FROM sys.dm_os_wait_stats ws LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 + WHERE wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0 AND i.wait_type IS NULL) BEGIN /* Check for waits that have had more than 10% of the server's wait time */ @@ -7197,7 +9874,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 (SELECT ws.wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms FROM sys.dm_os_wait_stats ws LEFT OUTER JOIN #IgnorableWaits i ON ws.wait_type = i.wait_type - WHERE i.wait_type IS NULL + WHERE i.wait_type IS NULL AND wait_time_ms > .1 * @CpuMsSinceWaitsCleared AND waiting_tasks_count > 0) INSERT INTO #BlitzResults @@ -7213,23 +9890,23 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 ,240 AS Priority ,'Wait Stats' AS FindingsGroup , CAST(ROW_NUMBER() OVER(ORDER BY os.wait_time_ms DESC) AS NVARCHAR(10)) + N' - ' + os.wait_type AS Finding - ,'https://BrentOzar.com/go/waits' AS URL + ,'https://www.sqlskills.com/help/waits/' + LOWER(os.wait_type) + '/' AS URL , Details = CAST(CAST(SUM(os.wait_time_ms / 1000.0 / 60 / 60) OVER (PARTITION BY os.wait_type) AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' hours of waits, ' + - CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + + CAST(CAST((SUM(60.0 * os.wait_time_ms) OVER (PARTITION BY os.wait_type) ) / @MsSinceWaitsCleared AS NUMERIC(18,1)) AS NVARCHAR(20)) + N' minutes average wait time per hour, ' + /* CAST(CAST( - 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) + 100.* SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) / (1. * SUM(os.wait_time_ms) OVER () ) AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% of waits, ' + */ CAST(CAST( - 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) + 100. * SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type) / (1. * SUM(os.wait_time_ms) OVER ()) - AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + + AS NUMERIC(18,1)) AS NVARCHAR(40)) + N'% signal wait, ' + CAST(SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS NVARCHAR(40)) + N' waiting tasks, ' + CAST(CASE WHEN SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) > 0 THEN CAST( SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) - / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) + / (1. * SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type)) AS NUMERIC(18,1)) ELSE 0 END AS NVARCHAR(40)) + N' ms average wait time.' FROM os @@ -7250,14 +9927,291 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 URL , Details ) - VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://BrentOzar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); + VALUES (153, 240, 'Wait Stats', 'No Significant Waits Detected', 'https://www.brentozar.com/go/waits', 'This server might be just sitting around idle, or someone may have cleared wait stats recently.'); END; - END; /* CheckID 152 */ + END; /* CheckID 152 */ + + /* CheckID 222 - Server Info - Azure Managed Instance */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 222 ) + AND 4 = ( SELECT COUNT(*) + FROM sys.all_objects o + INNER JOIN sys.all_columns c ON o.object_id = c.object_id + WHERE o.name = 'dm_os_job_object' + AND c.name IN ('cpu_rate', 'memory_limit_mb', 'process_memory_limit_mb', 'workingset_limit_mb' )) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 222) WITH NOWAIT; + + SET @StringToExecute = 'INSERT INTO #BlitzResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 222 AS CheckID , + 250 AS Priority , + ''Server Info'' AS FindingsGroup , + ''Azure Managed Instance'' AS Finding , + ''https://www.BrentOzar.com/go/azurevm'' AS URL , + ''cpu_rate: '' + CAST(COALESCE(cpu_rate, 0) AS VARCHAR(20)) + + '', memory_limit_mb: '' + CAST(COALESCE(memory_limit_mb, 0) AS NVARCHAR(20)) + + '', process_memory_limit_mb: '' + CAST(COALESCE(process_memory_limit_mb, 0) AS NVARCHAR(20)) + + '', workingset_limit_mb: '' + CAST(COALESCE(workingset_limit_mb, 0) AS NVARCHAR(20)) + FROM sys.dm_os_job_object 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.'; + + EXECUTE(@StringToExecute); + END; + + /* CheckID 224 - Performance - SSRS/SSAS/SSIS Installed */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 224 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 224) WITH NOWAIT; + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + BEGIN + + IF OBJECT_ID('tempdb..#services') IS NOT NULL DROP TABLE #services; + CREATE TABLE #services (cmdshell_output varchar(max)); + + INSERT INTO #services + EXEC /**/xp_cmdshell/**/ 'net start' /* added comments around command since some firewalls block this string TL 20210221 */ + + IF EXISTS (SELECT 1 + FROM #services + WHERE cmdshell_output LIKE '%SQL Server Reporting Services%' + OR cmdshell_output LIKE '%SQL Server Integration Services%' + OR cmdshell_output LIKE '%SQL Server Analysis Services%') + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 224 AS CheckID + ,200 AS Priority + ,'Performance' AS FindingsGroup + ,'SSAS/SSIS/SSRS Installed' AS Finding + ,'https://www.BrentOzar.com/go/services' AS URL + ,'Did you know you have other SQL Server services installed on this box other than the engine? It can be a real performance pain.' as Details + + END; + + END; + END; + + /* CheckID 232 - Server Info - Data Size */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 232 ) + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 232) WITH NOWAIT; + + IF OBJECT_ID('tempdb..#MasterFiles') IS NOT NULL + DROP TABLE #MasterFiles; + CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); + /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ + IF ((SERVERPROPERTY('Edition')) = 'SQL Azure' + AND (OBJECT_ID('sys.master_files') IS NULL)) + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;'; + ELSE + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;'; + EXEC(@StringToExecute); + + + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 232 AS CheckID + ,250 AS Priority + ,'Server Info' AS FindingsGroup + ,'Data Size' AS Finding + ,'' AS URL + ,CAST(COUNT(DISTINCT database_id) AS NVARCHAR(100)) + N' databases, ' + CAST(CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS MONEY) AS VARCHAR(100)) + ' GB total file size' as Details + FROM #MasterFiles + WHERE database_id > 4; + + END; + + /* CheckID 260 - Security - SQL Server service account is member of Administrators */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 260 ) AND @ProductVersionMajor >= 10 + BEGIN + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + AND EXISTS ( SELECT 1 FROM sys.all_objects WHERE [name] = 'dm_server_services' ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 260) WITH NOWAIT; + IF OBJECT_ID('tempdb..#localadmins') IS NOT NULL DROP TABLE #localadmins; + CREATE TABLE #localadmins (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 */ + + IF EXISTS (SELECT 1 + FROM #localadmins + 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 Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 260 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server''s service account is a member of the local Administrators group - meaning that anyone who can use xp_cmdshell can do anything on the host.' as Details + + END; + + END; + END; + + /* CheckID 261 - Security - SQL Server Agent service account is member of Administrators */ + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 261 ) AND @ProductVersionMajor >= 10 + BEGIN + + IF (SELECT value_in_use FROM sys.configurations WHERE [name] = 'xp_cmdshell') = 1 + AND EXISTS ( SELECT 1 FROM sys.all_objects WHERE [name] = 'dm_server_services' ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 261) WITH NOWAIT; + /*If this table exists and CheckId 260 was not skipped, then we're piggybacking off of 260's results */ + IF OBJECT_ID('tempdb..#localadmins') IS NOT NULL + AND NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 260 ) + BEGIN + IF @Debug IN (1, 2) RAISERROR('CheckId [%d] - found #localadmins table from CheckID 260 - no need to call xp_cmdshell again', 0, 1, 261) WITH NOWAIT; + + IF EXISTS (SELECT 1 + FROM #localadmins + WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) + FROM [sys].[dm_server_services] + WHERE [servicename] LIKE 'SQL Server%Agent%' + AND [servicename] NOT LIKE 'SQL Server Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 261 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server Agent''s service account is a member of the local Administrators group - meaning that anyone who can create and run jobs can do anything on the host.' as Details + + END; + END; /*piggyback*/ + ELSE /*can't piggyback*/ + BEGIN + /*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)); + /* 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 + WHERE LOWER(cmdshell_output) = ( SELECT LOWER([service_account]) + FROM [sys].[dm_server_services] + WHERE [servicename] LIKE 'SQL Server%Agent%' + AND [servicename] NOT LIKE 'SQL Server Launchpad%')) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT + 261 AS CheckID + ,1 AS Priority + ,'Security' AS FindingsGroup + ,'Dangerous Service Account' AS Finding + ,'https://vladdba.com/SQLServerSvcAccount' AS URL + ,'SQL Server Agent''s service account is a member of the local Administrators group - meaning that anyone who can create and run jobs can do anything on the host.' as Details + + END; + + END;/*can't piggyback*/ + END; + END; /* CheckID 261 */ + + + IF NOT EXISTS ( SELECT 1 + FROM #SkipChecks + WHERE DatabaseName IS NULL AND CheckID = 266 ) + BEGIN + INSERT INTO #BlitzResults + ( CheckID , + Priority , + FindingsGroup , + Finding , + URL , + Details + ) + SELECT 266 AS CheckID , + 250 AS Priority , + 'Server Info' AS FindingsGroup , + 'Hardware - Memory Counters' AS Finding , + 'https://www.brentozar.com/go/target' AS URL , + N'Target Server Memory (GB): ' + CAST((CAST((pTarget.cntr_value / 1024.0 / 1024.0) AS DECIMAL(10,1))) AS NVARCHAR(100)) + + N' Total Server Memory (GB): ' + CAST((CAST((pTotal.cntr_value / 1024.0 / 1024.0) AS DECIMAL(10,1))) AS NVARCHAR(100)) + FROM sys.dm_os_performance_counters pTarget + INNER JOIN sys.dm_os_performance_counters pTotal + ON pTotal.object_name LIKE 'SQLServer:Memory Manager%' + AND pTotal.counter_name LIKE 'Total Server Memory (KB)%' + WHERE pTarget.object_name LIKE 'SQLServer:Memory Manager%' + AND pTarget.counter_name LIKE 'Target Server Memory (KB)%' + END + + END; /* IF @CheckServerInfo = 1 */ END; /* IF ( ( SERVERPROPERTY('ServerName') NOT IN ( SELECT ServerName */ - /* Delete priorites they wanted to skip. */ IF @IgnorePrioritiesAbove IS NOT NULL DELETE #BlitzResults @@ -7351,7 +10305,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 GETDATE() , 'http://FirstResponderKit.org/' , 'Captain''s log: stardate something and something...'; - + IF @EmailRecipients IS NOT NULL BEGIN @@ -7361,7 +10315,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 IF (OBJECT_ID('tempdb..##BlitzResults', 'U') IS NOT NULL) DROP TABLE ##BlitzResults; SELECT * INTO ##BlitzResults FROM #BlitzResults; SET @query_result_separator = char(9); - SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup, Finding, Details; SET NOCOUNT OFF;'; + SET @StringToExecute = 'SET NOCOUNT ON;SELECT [Priority] , [FindingsGroup] , [Finding] , [DatabaseName] , [URL] , [Details] , CheckID FROM ##BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details; SET NOCOUNT OFF;'; SET @EmailSubject = 'sp_Blitz Results for ' + @@SERVERNAME; SET @EmailBody = 'sp_Blitz ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)) + '. http://FirstResponderKit.org'; IF @EmailProfile IS NULL @@ -7445,7 +10399,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 END; ELSE BEGIN - SET @ValidOutputLocation = 0; + SET @ValidOutputLocation = 0; END; END; @@ -7484,10 +10438,14 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); - END; + END; ELSE BEGIN - EXEC(@StringToExecute); + IF @OutputXMLasNVARCHAR = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'[XML]','[NVARCHAR](MAX)'); + END; + EXEC(@StringToExecute); END; IF @ValidOutputServer = 1 BEGIN @@ -7502,12 +10460,27 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + @OutputTableName + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; EXEC(@StringToExecute); - END; + END; ELSE BEGIN + IF @OutputXMLasNVARCHAR = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, CAST(QueryPlan AS NVARCHAR(MAX)), QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; + END; + ELSE + begin SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' @@ -7517,9 +10490,10 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + @OutputTableName + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; - + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; + END; EXEC(@StringToExecute); + END; END; ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') @@ -7552,7 +10526,7 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 + @OutputTableName + ' (ServerName, CheckDate, CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered) SELECT ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + ''', SYSDATETIMEOFFSET(), CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details, QueryPlan, QueryPlanFiltered FROM #BlitzResults ORDER BY Priority , FindingsGroup , Finding , DatabaseName , Details'; EXEC(@StringToExecute); END; @@ -7562,7 +10536,6 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); END; - DECLARE @separator AS VARCHAR(1); IF @OutputType = 'RSV' SET @separator = CHAR(31); @@ -7613,24 +10586,45 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 END; ELSE IF @OutputType = 'MARKDOWN' BEGIN - WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * + WITH Results AS (SELECT row_number() OVER (ORDER BY Priority, FindingsGroup, Finding, DatabaseName, Details) AS rownum, * FROM #BlitzResults WHERE Priority > 0 AND Priority < 255 AND FindingsGroup IS NOT NULL AND Finding IS NOT NULL AND FindingsGroup <> 'Security' /* Specifically excluding security checks for public exports */) SELECT - CASE - WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf - ELSE N'' - END - + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf - WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END - ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END - END + @crlf - FROM Results r - LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 - LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 - ORDER BY r.rownum FOR XML PATH(N''); + Markdown = CONVERT(XML, STUFF((SELECT + CASE + WHEN r.Priority <> COALESCE(rPrior.Priority, 0) OR r.FindingsGroup <> rPrior.FindingsGroup THEN @crlf + N'**Priority ' + CAST(COALESCE(r.Priority,N'') AS NVARCHAR(5)) + N': ' + COALESCE(r.FindingsGroup,N'') + N'**:' + @crlf + @crlf + ELSE N'' + END + + CASE WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding <> COALESCE(rNext.Finding,N'') THEN N'- ' + COALESCE(r.Finding,N'') + N' ' + COALESCE(r.DatabaseName, N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding AND r.Details = rNext.Details THEN N'- ' + COALESCE(r.Finding,N'') + N' - ' + COALESCE(r.Details,N'') + @crlf + @crlf + N' * ' + COALESCE(r.DatabaseName, N'') + @crlf + WHEN r.Finding <> COALESCE(rPrior.Finding,N'') AND r.Finding = rNext.Finding THEN N'- ' + COALESCE(r.Finding,N'') + @crlf + @crlf + CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE '' END + ELSE CASE WHEN r.DatabaseName IS NULL THEN N'' ELSE N' * ' + COALESCE(r.DatabaseName,N'') END + CASE WHEN r.Details <> rPrior.Details THEN N' - ' + COALESCE(r.Details,N'') + @crlf ELSE N'' + @crlf END + END + @crlf + FROM Results r + LEFT OUTER JOIN Results rPrior ON r.rownum = rPrior.rownum + 1 + LEFT OUTER JOIN Results rNext ON r.rownum = rNext.rownum - 1 + ORDER BY r.rownum FOR XML PATH(N''), ROOT('Markdown'), TYPE).value('/Markdown[1]','VARCHAR(MAX)'), 1, 2, '') + + ''); + END; + ELSE IF @OutputType = 'XML' + BEGIN + /* --TOURSTOP05-- */ + SELECT [Priority] , + [FindingsGroup] , + [Finding] , + [DatabaseName] , + [URL] , + [Details] , + [QueryPlanFiltered] , + CheckID + FROM #BlitzResults + ORDER BY Priority , + FindingsGroup , + Finding , + DatabaseName , + Details + FOR XML PATH('Result'), ROOT('sp_Blitz_Output'); END; ELSE IF @OutputType <> 'NONE' BEGIN @@ -7707,12 +10701,33 @@ IF @ProductVersionMajor >= 10 AND NOT EXISTS ( SELECT 1 END; /* ELSE -- IF @OutputType = 'SCHEMA' */ + /* + Cleanups - drop temporary tables that have been created by this SP. + */ + + IF(OBJECT_ID('tempdb..#InvalidLogins') IS NOT NULL) + BEGIN + EXEC sp_executesql N'DROP TABLE #InvalidLogins;'; + END; + + IF OBJECT_ID('tempdb..#AlertInfo') IS NOT NULL + BEGIN + EXEC sp_executesql N'DROP TABLE #AlertInfo;'; + END; + + /* + Reset the Nmumeric_RoundAbort session state back to enabled if it was disabled earlier. + See Github issue #2302 for more info. + */ + IF @NeedToTurnNumericRoundabortBackOn = 1 + SET NUMERIC_ROUNDABORT ON; + SET NOCOUNT OFF; GO /* --Sample execution call with the most common parameters: -EXEC [dbo].[sp_Blitz] +EXEC [dbo].[sp_Blitz] @CheckUserDatabaseObjects = 1 , @CheckProcedureCache = 0 , @OutputType = 'TABLE' , diff --git a/sp_BlitzAnalysis.sql b/sp_BlitzAnalysis.sql new file mode 100644 index 000000000..09f11f49e --- /dev/null +++ b/sp_BlitzAnalysis.sql @@ -0,0 +1,891 @@ +SET ANSI_NULLS ON; +SET QUOTED_IDENTIFIER ON + +IF NOT EXISTS (SELECT * FROM sys.objects WHERE [object_id] = OBJECT_ID(N'[dbo].[sp_BlitzAnalysis]') AND [type] in (N'P', N'PC')) +BEGIN +EXEC dbo.sp_executesql @statement = N'CREATE PROCEDURE [dbo].[sp_BlitzAnalysis] AS' +END +GO + +ALTER PROCEDURE [dbo].[sp_BlitzAnalysis] ( +@Help TINYINT = 0, +@StartDate DATETIMEOFFSET(7) = NULL, +@EndDate DATETIMEOFFSET(7) = NULL, +@OutputDatabaseName NVARCHAR(256) = 'DBAtools', +@OutputSchemaName NVARCHAR(256) = N'dbo', +@OutputTableNameBlitzFirst NVARCHAR(256) = N'BlitzFirst', +@OutputTableNameFileStats NVARCHAR(256) = N'BlitzFirst_FileStats', +@OutputTableNamePerfmonStats NVARCHAR(256) = N'BlitzFirst_PerfmonStats', +@OutputTableNameWaitStats NVARCHAR(256) = N'BlitzFirst_WaitStats', +@OutputTableNameBlitzCache NVARCHAR(256) = N'BlitzCache', +@OutputTableNameBlitzWho NVARCHAR(256) = N'BlitzWho', +@Servername NVARCHAR(128) = @@SERVERNAME, +@Databasename NVARCHAR(128) = NULL, +@BlitzCacheSortorder NVARCHAR(20) = N'cpu', +@MaxBlitzFirstPriority INT = 249, +@ReadLatencyThreshold INT = 100, +@WriteLatencyThreshold INT = 100, +@WaitStatsTop TINYINT = 10, +@Version VARCHAR(30) = NULL OUTPUT, +@VersionDate DATETIME = NULL OUTPUT, +@VersionCheckMode BIT = 0, +@BringThePain BIT = 0, +@Maxdop INT = 1, +@Debug BIT = 0 +) +AS +SET NOCOUNT ON; +SET STATISTICS XML OFF; + +SELECT @Version = '8.26', @VersionDate = '20251002'; + +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +IF (@Help = 1) +BEGIN + PRINT 'EXEC sp_BlitzAnalysis +@StartDate = NULL, /* Specify a datetime or NULL will get an hour ago */ +@EndDate = NULL, /* Specify a datetime or NULL will get an hour of data since @StartDate */ +@OutputDatabaseName = N''DBA'', /* Specify the database name where where we can find your logged blitz data */ +@OutputSchemaName = N''dbo'', /* Specify the schema */ +@OutputTableNameBlitzFirst = N''BlitzFirst'', /* Table name where you are storing sp_BlitzFirst output, Set to NULL to ignore */ +@OutputTableNameFileStats = N''BlitzFirst_FileStats'', /* Table name where you are storing sp_BlitzFirst filestats output, Set to NULL to ignore */ +@OutputTableNamePerfmonStats = N''BlitzFirst_PerfmonStats'', /* Table name where you are storing sp_BlitzFirst Perfmon output, Set to NULL to ignore */ +@OutputTableNameWaitStats = N''BlitzFirst_WaitStats'', /* Table name where you are storing sp_BlitzFirst Wait stats output, Set to NULL to ignore */ +@OutputTableNameBlitzCache = N''BlitzCache'', /* Table name where you are storing sp_BlitzCache output, Set to NULL to ignore */ +@OutputTableNameBlitzWho = N''BlitzWho'', /* Table name where you are storing sp_BlitzWho output, Set to NULL to ignore */ +@Databasename = NULL, /* Filters results for BlitzCache, FileStats (will also include tempdb), BlitzWho. Leave as NULL for all databases */ +@MaxBlitzFirstPriority = 249, /* Max priority to include in the results */ +@BlitzCacheSortorder = ''cpu'', /* Accepted values ''all'' ''cpu'' ''reads'' ''writes'' ''duration'' ''executions'' ''memory grant'' ''spills'' */ +@WaitStatsTop = 3, /* Controls the top for wait stats only */ +@Maxdop = 1, /* Control the degree of parallelism that the queries within this proc can use if they want to*/ +@Debug = 0; /* Show sp_BlitzAnalysis SQL commands in the messages tab as they execute */ + +/* +Additional parameters: +@ReadLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_read_average_ms in your filestats table */ +@WriteLatencyThreshold INT /* Default: 100 - Sets the threshold in ms to compare against io_stall_write_average_ms in your filestats table */ +@BringThePain BIT /* Default: 0 - If you are getting more than 4 hours of data with blitzcachesortorder set to ''all'' you will need to set BringThePain to 1 */ +*/'; + RETURN; +END + +/* Declare all local variables required */ +DECLARE @FullOutputTableNameBlitzFirst NVARCHAR(1000); +DECLARE @FullOutputTableNameFileStats NVARCHAR(1000); +DECLARE @FullOutputTableNamePerfmonStats NVARCHAR(1000); +DECLARE @FullOutputTableNameWaitStats NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzCache NVARCHAR(1000); +DECLARE @FullOutputTableNameBlitzWho NVARCHAR(1000); +DECLARE @Sql NVARCHAR(MAX); +DECLARE @NewLine NVARCHAR(2) = CHAR(13); +DECLARE @IncludeMemoryGrants BIT; +DECLARE @IncludeSpills BIT; + +/* Validate the database name */ +IF (DB_ID(@OutputDatabaseName) IS NULL) +BEGIN + RAISERROR('Invalid database name provided for parameter @OutputDatabaseName: %s',11,0,@OutputDatabaseName); + RETURN; +END + +/* Set fully qualified table names */ +SET @FullOutputTableNameBlitzFirst = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzFirst); +SET @FullOutputTableNameFileStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameFileStats+N'_Deltas'); +SET @FullOutputTableNamePerfmonStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNamePerfmonStats+N'_Actuals'); +SET @FullOutputTableNameWaitStats = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameWaitStats+N'_Deltas'); +SET @FullOutputTableNameBlitzCache = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzCache); +SET @FullOutputTableNameBlitzWho = QUOTENAME(@OutputDatabaseName)+N'.'+QUOTENAME(@OutputSchemaName)+N'.'+QUOTENAME(@OutputTableNameBlitzWho+N'_Deltas'); + +IF OBJECT_ID('tempdb.dbo.#BlitzFirstCounts') IS NOT NULL +BEGIN + DROP TABLE #BlitzFirstCounts; +END + +CREATE TABLE #BlitzFirstCounts ( + [Priority] TINYINT NOT NULL, + [FindingsGroup] VARCHAR(50) NOT NULL, + [Finding] VARCHAR(200) NOT NULL, + [TotalOccurrences] INT NULL, + [FirstOccurrence] DATETIMEOFFSET(7) NULL, + [LastOccurrence] DATETIMEOFFSET(7) NULL +); + +/* Validate variables and set defaults as required */ +IF (@BlitzCacheSortorder IS NULL) +BEGIN + SET @BlitzCacheSortorder = N'cpu'; +END + +SET @BlitzCacheSortorder = LOWER(@BlitzCacheSortorder); + +IF (@OutputTableNameBlitzCache IS NOT NULL AND @BlitzCacheSortorder NOT IN (N'all',N'cpu',N'reads',N'writes',N'duration',N'executions',N'memory grant',N'spills')) +BEGIN + RAISERROR('Invalid sort option specified for @BlitzCacheSortorder, supported values are ''all'', ''cpu'', ''reads'', ''writes'', ''duration'', ''executions'', ''memory grant'', ''spills''',11,0) WITH NOWAIT; + RETURN; +END + +/* Set @Maxdop to 1 if NULL was passed in */ +IF (@Maxdop IS NULL) +BEGIN + SET @Maxdop = 1; +END + +/* iF @Maxdop is set higher than the core count just set it to 0 */ +IF (@Maxdop > (SELECT CAST(cpu_count AS INT) FROM sys.dm_os_sys_info)) +BEGIN + SET @Maxdop = 0; +END + +/* We need to check if your SQL version has memory grant and spills columns in sys.dm_exec_query_stats */ +SELECT @IncludeMemoryGrants = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb')) THEN 1 + ELSE 0 + END; + +SELECT @IncludeSpills = + CASE + WHEN (EXISTS(SELECT * FROM sys.all_columns WHERE [object_id] = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills')) THEN 1 + ELSE 0 + END; + + +IF (@StartDate IS NULL) +BEGIN + RAISERROR('Setting @StartDate to: 1 hour ago',0,0) WITH NOWAIT; + /* Set StartDate to be an hour ago */ + SET @StartDate = DATEADD(HOUR,-1,SYSDATETIMEOFFSET()); + + IF (@EndDate IS NULL) + BEGIN + RAISERROR('Setting @EndDate to: Now',0,0) WITH NOWAIT; + /* Get data right up to now */ + SET @EndDate = SYSDATETIMEOFFSET(); + END +END + +IF (@EndDate IS NULL) +BEGIN + /* Default to an hour of data or SYSDATETIMEOFFSET() if now is earlier than the hour added to @StartDate */ + IF(DATEADD(HOUR,1,@StartDate) < SYSDATETIMEOFFSET()) + BEGIN + RAISERROR('@EndDate was NULL - Setting to return 1 hour of information, if you want more then set @EndDate aswell',0,0) WITH NOWAIT; + SET @EndDate = DATEADD(HOUR,1,@StartDate); + END + ELSE + BEGIN + RAISERROR('@EndDate was NULL - Setting to SYSDATETIMEOFFSET()',0,0) WITH NOWAIT; + SET @EndDate = SYSDATETIMEOFFSET(); + END +END + +/* Default to dbo schema if NULL is passed in */ +IF (@OutputSchemaName IS NULL) +BEGIN + SET @OutputSchemaName = 'dbo'; +END + +/* Prompt the user for @BringThePain = 1 if they are searching a timeframe greater than 4 hours and they are using BlitzCacheSortorder = 'all' */ +IF(@BlitzCacheSortorder = 'all' AND DATEDIFF(HOUR,@StartDate,@EndDate) > 4 AND @BringThePain = 0) +BEGIN + RAISERROR('Wow! hold up now, are you sure you wanna do this? Are sure you want to query over 4 hours of data with @BlitzCacheSortorder set to ''all''? IF you do then set @BringThePain = 1 but I gotta warn you this might hurt a bit!',11,1) WITH NOWAIT; + RETURN; +END + +/* Output report window information */ +SELECT + @Servername AS [ServerToReportOn], + CAST(1 AS NVARCHAR(20)) + N' - '+ CAST(@MaxBlitzFirstPriority AS NVARCHAR(20)) AS [PrioritesToInclude], + @StartDate AS [StartDatetime], + @EndDate AS [EndDatetime];; + + +/* BlitzFirst data */ +SET @Sql = N' +INSERT INTO #BlitzFirstCounts ([Priority],[FindingsGroup],[Finding],[TotalOccurrences],[FirstOccurrence],[LastOccurrence]) +SELECT +[Priority], +[FindingsGroup], +[Finding], +COUNT(*) AS [TotalOccurrences], +MIN(CheckDate) AS [FirstOccurrence], +MAX(CheckDate) AS [LastOccurrence] +FROM '+@FullOutputTableNameBlitzFirst+N' +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND CheckDate BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +GROUP BY [Priority],[FindingsGroup],[Finding]; + +IF EXISTS(SELECT 1 FROM #BlitzFirstCounts) +BEGIN + SELECT + [Priority], + [FindingsGroup], + [Finding], + [TotalOccurrences], + [FirstOccurrence], + [LastOccurrence] + FROM #BlitzFirstCounts + ORDER BY [Priority] ASC,[TotalOccurrences] DESC; +END +ELSE +BEGIN + SELECT N''No findings with a priority between 1 and ''+CAST(@MaxBlitzFirstPriority AS NVARCHAR(10))+N'' found for this period''; +END +SELECT + [ServerName] +,[CheckDate] +,[CheckID] +,[Priority] +,[Finding] +,[URL] +,[Details] +,[HowToStopIt] +,[QueryPlan] +,[QueryText] +FROM '+@FullOutputTableNameBlitzFirst+N' Findings +WHERE [ServerName] = @Servername +AND [Priority] BETWEEN 1 AND @MaxBlitzFirstPriority +AND [CheckDate] BETWEEN @StartDate AND @EndDate +AND [CheckID] > -1 +ORDER BY CheckDate ASC,[Priority] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + + +RAISERROR('Getting BlitzFirst info from %s',0,0,@FullOutputTableNameBlitzFirst) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzFirst) IS NULL) +BEGIN + IF (@OutputTableNameBlitzFirst IS NULL) + BEGIN + RAISERROR('BlitzFirst data skipped',10,0); + SELECT N'Skipped logged BlitzFirst data as NULL was passed to parameter @OutputTableNameBlitzFirst'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzFirst data: %s does not exist',10,0,@FullOutputTableNameBlitzFirst); + SELECT N'No BlitzFirst data available as the table cannot be found'; + END + +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @MaxBlitzFirstPriority INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @MaxBlitzFirstPriority = @MaxBlitzFirstPriority; +END + +/* Blitz WaitStats data */ +SET @Sql = N'SELECT +[ServerName], +[CheckDate], +[wait_type], +[WaitsRank], +[WaitCategory], +[Ignorable], +[ElapsedSeconds], +[wait_time_ms_delta], +[wait_time_minutes_delta], +[wait_time_minutes_per_minute], +[signal_wait_time_ms_delta], +[waiting_tasks_count_delta], +ISNULL((CAST([wait_time_ms_delta] AS DECIMAL(38,2))/NULLIF(CAST([waiting_tasks_count_delta] AS DECIMAL(38,2)),0)),0) AS [wait_time_ms_per_wait] +FROM +( + SELECT + [ServerName], + [CheckDate], + [wait_type], + [WaitCategory], + [Ignorable], + [ElapsedSeconds], + [wait_time_ms_delta], + [wait_time_minutes_delta], + [wait_time_minutes_per_minute], + [signal_wait_time_ms_delta], + [waiting_tasks_count_delta], + ROW_NUMBER() OVER(PARTITION BY [CheckDate] ORDER BY [CheckDate] ASC,[wait_time_ms_delta] DESC) AS [WaitsRank] + FROM '+@FullOutputTableNameWaitStats+N' AS [Waits] + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate +) TopWaits +WHERE [WaitsRank] <= @WaitStatsTop +ORDER BY +[CheckDate] ASC, +[wait_time_ms_delta] DESC +OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting wait stats info from %s',0,0,@FullOutputTableNameWaitStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameWaitStats) IS NULL) +BEGIN + IF (@OutputTableNameWaitStats IS NULL) + BEGIN + RAISERROR('Wait stats data skipped',10,0); + SELECT N'Skipped logged wait stats data as NULL was passed to parameter @OutputTableNameWaitStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for wait stats data: %s does not exist',10,0,@FullOutputTableNameWaitStats); + SELECT N'No wait stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @WaitStatsTop TINYINT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @WaitStatsTop=@WaitStatsTop; +END + +/* BlitzFileStats info */ +SET @Sql = N' +SELECT +[ServerName], +[CheckDate], +CASE + WHEN MAX([io_stall_read_ms_average]) > @ReadLatencyThreshold THEN ''Yes'' + WHEN MAX([io_stall_write_ms_average]) > @WriteLatencyThreshold THEN ''Yes'' + ELSE ''No'' +END AS [io_stall_ms_breached], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) AS [PhysicalPath], +SUM([SizeOnDiskMB]) AS [SizeOnDiskMB], +SUM([SizeOnDiskMBgrowth]) AS [SizeOnDiskMBgrowth], +MAX([io_stall_read_ms]) AS [max_io_stall_read_ms], +MAX([io_stall_read_ms_average]) AS [max_io_stall_read_ms_average], +@ReadLatencyThreshold AS [is_stall_read_ms_threshold], +SUM([num_of_reads]) AS [num_of_reads], +SUM([megabytes_read]) AS [megabytes_read], +MAX([io_stall_write_ms]) AS [max_io_stall_write_ms], +MAX([io_stall_write_ms_average]) AS [max_io_stall_write_ms_average], +@WriteLatencyThreshold AS [io_stall_write_ms_average], +SUM([num_of_writes]) AS [num_of_writes], +SUM([megabytes_written]) AS [megabytes_written] +FROM '+@FullOutputTableNameFileStats+N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate +' ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] IN (N''tempdb'',@Databasename) +' + ELSE N'' +END ++N'GROUP BY +[ServerName], +[CheckDate], +LEFT([PhysicalName],LEN([PhysicalName])-CHARINDEX(''\'',REVERSE([PhysicalName]))+1) +ORDER BY +[CheckDate] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting FileStats info from %s',0,0,@FullOutputTableNameFileStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameFileStats) IS NULL) +BEGIN + IF (@OutputTableNameFileStats IS NULL) + BEGIN + RAISERROR('File stats data skipped',10,0); + SELECT N'Skipped logged File stats data as NULL was passed to parameter @OutputTableNameFileStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for FileStats data: %s does not exist',10,0,@FullOutputTableNameFileStats); + SELECT N'No File stats data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @ReadLatencyThreshold INT, + @WriteLatencyThreshold INT', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename, + @ReadLatencyThreshold = @ReadLatencyThreshold, + @WriteLatencyThreshold = @WriteLatencyThreshold; +END + +/* Blitz Perfmon stats*/ +SET @Sql = N' +SELECT + [ServerName] + ,[CheckDate] + ,[counter_name] + ,[object_name] + ,[instance_name] + ,[cntr_value] +FROM '+@FullOutputTableNamePerfmonStats+N' +WHERE [ServerName] = @Servername +AND CheckDate BETWEEN @StartDate AND @EndDate +ORDER BY + [CheckDate] ASC, + [counter_name] ASC +OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');' + +RAISERROR('Getting Perfmon info from %s',0,0,@FullOutputTableNamePerfmonStats) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNamePerfmonStats) IS NULL) +BEGIN + IF (@OutputTableNamePerfmonStats IS NULL) + BEGIN + RAISERROR('Perfmon stats data skipped',10,0); + SELECT N'Skipped logged Perfmon stats data as NULL was passed to parameter @OutputTableNamePerfmonStats'; + END + ELSE + BEGIN + RAISERROR('Table provided for Perfmon stats data: %s does not exist',10,0,@FullOutputTableNamePerfmonStats); + SELECT N'No Perfmon data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername; +END + +/* Blitz cache data */ +RAISERROR('Sortorder for BlitzCache data: %s',0,0,@BlitzCacheSortorder) WITH NOWAIT; + +/* Set intial CTE */ +SET @Sql = N'WITH CheckDates AS ( +SELECT DISTINCT CheckDate +FROM ' ++@FullOutputTableNameBlitzCache ++N' +WHERE [ServerName] = @Servername +AND [CheckDate] BETWEEN @StartDate AND @EndDate' ++@NewLine ++CASE + WHEN @Databasename IS NOT NULL THEN N'AND [DatabaseName] = @Databasename'+@NewLine + ELSE N'' +END ++N')' +; + +SET @Sql += @NewLine; + +/* Append additional CTEs based on sortorder */ +SET @Sql += ( +SELECT CAST(N',' AS NVARCHAR(MAX)) ++[SortOptions].[Aliasname]+N' AS ( +SELECT + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,[Sortorder] + ,[TimeFrameRank] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [OverallRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] +FROM CheckDates +CROSS APPLY ( + SELECT TOP (5) + [ServerName] + ,'+[SortOptions].[Aliasname]+N'.[CheckDate] + ,'+QUOTENAME(UPPER([SortOptions].[Sortorder]),N'''')+N' AS [Sortorder] + ,ROW_NUMBER() OVER(ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) AS [TimeFrameRank] + ,'+[SortOptions].[Aliasname]+N'.[QueryType] + ,'+[SortOptions].[Aliasname]+N'.[QueryText] + ,'+[SortOptions].[Aliasname]+N'.[DatabaseName] + ,'+[SortOptions].[Aliasname]+N'.[AverageCPU] + ,'+[SortOptions].[Aliasname]+N'.[TotalCPU] + ,'+[SortOptions].[Aliasname]+N'.[PercentCPUByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageDuration] + ,'+[SortOptions].[Aliasname]+N'.[TotalDuration] + ,'+[SortOptions].[Aliasname]+N'.[PercentDurationByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageReads] + ,'+[SortOptions].[Aliasname]+N'.[TotalReads] + ,'+[SortOptions].[Aliasname]+N'.[PercentReadsByType] + ,'+[SortOptions].[Aliasname]+N'.[AverageWrites] + ,'+[SortOptions].[Aliasname]+N'.[TotalWrites] + ,'+[SortOptions].[Aliasname]+N'.[PercentWritesByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionCount] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionWeight] + ,'+[SortOptions].[Aliasname]+N'.[PercentExecutionsByType] + ,'+[SortOptions].[Aliasname]+N'.[ExecutionsPerMinute] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanCreationTimeHours] + ,'+[SortOptions].[Aliasname]+N'.[LastExecutionTime] + ,'+[SortOptions].[Aliasname]+N'.[PlanHandle] + ,'+[SortOptions].[Aliasname]+N'.[SqlHandle] + ,'+[SortOptions].[Aliasname]+N'.[SQL Handle More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryHash] + ,'+[SortOptions].[Aliasname]+N'.[Query Hash More Info] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanHash] + ,'+[SortOptions].[Aliasname]+N'.[StatementStartOffset] + ,'+[SortOptions].[Aliasname]+N'.[StatementEndOffset] + ,'+[SortOptions].[Aliasname]+N'.[MinReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[MaxReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[AverageReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[TotalReturnedRows] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlan] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfPlans] + ,'+[SortOptions].[Aliasname]+N'.[NumberOfDistinctPlans] + ,'+[SortOptions].[Aliasname]+N'.[MinGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MinUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[MaxUsedGrantKB] + ,'+[SortOptions].[Aliasname]+N'.[PercentMemoryGrantUsed] + ,'+[SortOptions].[Aliasname]+N'.[AvgMaxMemoryGrant] + ,'+[SortOptions].[Aliasname]+N'.[MinSpills] + ,'+[SortOptions].[Aliasname]+N'.[MaxSpills] + ,'+[SortOptions].[Aliasname]+N'.[TotalSpills] + ,'+[SortOptions].[Aliasname]+N'.[AvgSpills] + ,'+[SortOptions].[Aliasname]+N'.[QueryPlanCost] + FROM '+@FullOutputTableNameBlitzCache+N' AS '+[SortOptions].[Aliasname]+N' + WHERE [ServerName] = @Servername + AND [CheckDate] BETWEEN @StartDate AND @EndDate + AND ['+[SortOptions].[Aliasname]+N'].[CheckDate] = [CheckDates].[CheckDate]' + +@NewLine + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND ['+[SortOptions].[Aliasname]+N'].[DatabaseName] = @Databasename'+@NewLine + ELSE N'' + END + +CASE + WHEN [Sortorder] = N'cpu' THEN N'AND [TotalCPU] > 0' + WHEN [Sortorder] = N'reads' THEN N'AND [TotalReads] > 0' + WHEN [Sortorder] = N'writes' THEN N'AND [TotalWrites] > 0' + WHEN [Sortorder] = N'duration' THEN N'AND [TotalDuration] > 0' + WHEN [Sortorder] = N'executions' THEN N'AND [ExecutionCount] > 0' + WHEN [Sortorder] = N'memory grant' THEN N'AND [MaxGrantKB] > 0' + WHEN [Sortorder] = N'spills' THEN N'AND [MaxSpills] > 0' + ELSE N'' + END + +N' + ORDER BY ['+[SortOptions].[Columnname]+N'] DESC) '+[SortOptions].[Aliasname]+N' +)' +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'); + +SET @Sql += @NewLine; + +/* Build the select statements to return the data after CTE declarations */ +SET @Sql += ( +SELECT STUFF(( +SELECT @NewLine ++N'UNION ALL' ++@NewLine ++N'SELECT * +FROM '+[SortOptions].[Aliasname] +FROM (VALUES + (N'cpu',N'TopCPU',N'TotalCPU'), + (N'reads',N'TopReads',N'TotalReads'), + (N'writes',N'TopWrites',N'TotalWrites'), + (N'duration',N'TopDuration',N'TotalDuration'), + (N'executions',N'TopExecutions',N'ExecutionCount'), + (N'memory grant',N'TopMemoryGrants',N'MaxGrantKB'), + (N'spills',N'TopSpills',N'MaxSpills') + ) SortOptions(Sortorder,Aliasname,Columnname) +WHERE + CASE /* for spills and memory grant sorts make sure the underlying columns exist in the DMV otherwise do not include them */ + WHEN (@IncludeMemoryGrants = 0 OR @IncludeMemoryGrants IS NULL) AND ([SortOptions].[Sortorder] = N'memory grant' OR [SortOptions].[Sortorder] = N'all') THEN NULL + WHEN (@IncludeSpills = 0 OR @IncludeSpills IS NULL) AND ([SortOptions].[Sortorder] = N'spills' OR [SortOptions].[Sortorder] = N'all') THEN NULL + ELSE [SortOptions].[Sortorder] + END = ISNULL(NULLIF(@BlitzCacheSortorder,N'all'),[SortOptions].[Sortorder]) +FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'),1,11,N'') +); + +/* Append Order By */ +SET @Sql += @NewLine ++N'ORDER BY + [Sortorder] ASC, + [CheckDate] ASC, + [TimeFrameRank] ASC'; + +/* Append OPTION(RECOMPILE, MAXDOP) to complete the statement */ +SET @Sql += @NewLine ++N'OPTION(RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + +RAISERROR('Getting BlitzCache info from %s',0,0,@FullOutputTableNameBlitzCache) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT SUBSTRING(@Sql, 0, 4000); + PRINT SUBSTRING(@Sql, 4000, 8000); + PRINT SUBSTRING(@Sql, 8000, 12000); + PRINT SUBSTRING(@Sql, 12000, 16000); + PRINT SUBSTRING(@Sql, 16000, 20000); + PRINT SUBSTRING(@Sql, 20000, 24000); + PRINT SUBSTRING(@Sql, 24000, 28000); +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzCache) IS NULL) +BEGIN + IF (@OutputTableNameBlitzCache IS NULL) + BEGIN + RAISERROR('BlitzCache data skipped',10,0); + SELECT N'Skipped logged BlitzCache data as NULL was passed to parameter @OutputTableNameBlitzCache'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzCache data: %s does not exist',10,0,@FullOutputTableNameBlitzCache); + SELECT N'No BlitzCache data available as the table cannot be found'; + END +END +ELSE /* Table exists then run the query */ +BEGIN + EXEC sp_executesql @Sql, + N'@Servername NVARCHAR(128), + @Databasename NVARCHAR(128), + @BlitzCacheSortorder NVARCHAR(20), + @StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7)', + @Servername = @Servername, + @Databasename = @Databasename, + @BlitzCacheSortorder = @BlitzCacheSortorder, + @StartDate = @StartDate, + @EndDate = @EndDate; +END + + + +/* BlitzWho data */ +SET @Sql = N' +SELECT [ServerName] + ,[CheckDate] + ,[elapsed_time] + ,[session_id] + ,[database_name] + ,[query_text_snippet] + ,[query_plan] + ,[live_query_plan] + ,[query_cost] + ,[status] + ,[wait_info] + ,[top_session_waits] + ,[blocking_session_id] + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[degree_of_parallelism] + ,[request_logical_reads] + ,[Logical_Reads_MB] + ,[request_writes] + ,[Logical_Writes_MB] + ,[request_physical_reads] + ,[Physical_reads_MB] + ,[session_cpu] + ,[session_logical_reads] + ,[session_logical_reads_MB] + ,[session_physical_reads] + ,[session_physical_reads_MB] + ,[session_writes] + ,[session_writes_MB] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads] + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info] + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset] + FROM '+@FullOutputTableNameBlitzWho+N' + WHERE [ServerName] = @Servername + AND ([CheckDate] BETWEEN @StartDate AND @EndDate OR [start_time] BETWEEN CAST(@StartDate AS DATETIME) AND CAST(@EndDate AS DATETIME)) + ' + +CASE + WHEN @Databasename IS NOT NULL THEN N'AND [database_name] = @Databasename + ' + ELSE N'' + END ++N'ORDER BY [CheckDate] ASC + OPTION (RECOMPILE, MAXDOP '+CAST(@Maxdop AS NVARCHAR(2))+N');'; + +RAISERROR('Getting BlitzWho info from %s',0,0,@FullOutputTableNameBlitzWho) WITH NOWAIT; + +IF (@Debug = 1) +BEGIN + PRINT @Sql; +END + +IF (OBJECT_ID(@FullOutputTableNameBlitzWho) IS NULL) +BEGIN + IF (@OutputTableNameBlitzWho IS NULL) + BEGIN + RAISERROR('BlitzWho data skipped',10,0); + SELECT N'Skipped logged BlitzWho data as NULL was passed to parameter @OutputTableNameBlitzWho'; + END + ELSE + BEGIN + RAISERROR('Table provided for BlitzWho data: %s does not exist',10,0,@FullOutputTableNameBlitzWho); + SELECT N'No BlitzWho data available as the table cannot be found'; + END +END +ELSE +BEGIN + EXEC sp_executesql @Sql, + N'@StartDate DATETIMEOFFSET(7), + @EndDate DATETIMEOFFSET(7), + @Servername NVARCHAR(128), + @Databasename NVARCHAR(128)', + @StartDate=@StartDate, + @EndDate=@EndDate, + @Servername=@Servername, + @Databasename = @Databasename; +END + + +GO diff --git a/sp_BlitzBackups.sql b/sp_BlitzBackups.sql index a538092cb..403199016 100755 --- a/sp_BlitzBackups.sql +++ b/sp_BlitzBackups.sql @@ -14,15 +14,22 @@ ALTER PROCEDURE [dbo].[sp_BlitzBackups] @WriteBackupsToListenerName NVARCHAR(256) = NULL, @WriteBackupsToDatabaseName NVARCHAR(256) = NULL, @WriteBackupsLastHours INT = 168, - @VersionDate DATE = NULL OUTPUT + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS BEGIN SET NOCOUNT ON; + SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; IF @Help = 1 PRINT ' /* @@ -64,7 +71,7 @@ AS MIT License - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -230,14 +237,14 @@ 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) ); IF NOT EXISTS(SELECT * FROM sys.databases WHERE name = @MSDBName) BEGIN - RAISERROR('@MSDBName was specified, but the database does not exist.', 0, 1) WITH NOWAIT; + RAISERROR('@MSDBName was specified, but the database does not exist.', 16, 1) WITH NOWAIT; RETURN; END @@ -1149,13 +1156,13 @@ DECLARE @RemoteCheck TABLE (c INT NULL); IF @WriteBackupsToDatabaseName IS NULL BEGIN - RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 0, 1) WITH NOWAIT + RAISERROR('@WriteBackupsToDatabaseName can''t be NULL.', 16, 1) WITH NOWAIT RETURN; END IF LOWER(@WriteBackupsToDatabaseName) = N'msdb' BEGIN - RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 0, 1) WITH NOWAIT + RAISERROR('We can''t write to the real msdb, we have to write to a fake msdb.', 16, 1) WITH NOWAIT RETURN; END @@ -1163,7 +1170,7 @@ IF @WriteBackupsToListenerName IS NULL BEGIN IF @AGName IS NULL BEGIN - RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 0, 1) WITH NOWAIT; + RAISERROR('@WriteBackupsToListenerName and @AGName can''t both be NULL.', 16, 1) WITH NOWAIT; RETURN; END ELSE @@ -1187,7 +1194,7 @@ BEGIN ) BEGIN SET @msg = N'We need a linked server to write data across. Please set one up for ' + @WriteBackupsToListenerName + N'.'; - RAISERROR(@msg, 0, 1) WITH NOWAIT; + RAISERROR(@msg, 16, 1) WITH NOWAIT; RETURN; END END @@ -1206,7 +1213,7 @@ END IF @@ROWCOUNT = 0 BEGIN SET @msg = N'The database ' + @WriteBackupsToDatabaseName + N' doesn''t appear to exist on that server.' - RAISERROR(@msg, 0, 1) WITH NOWAIT + RAISERROR(@msg, 16, 1) WITH NOWAIT RETURN; END @@ -1458,7 +1465,7 @@ END SET @StringToExecute += N'INSERT ' + QUOTENAME(@WriteBackupsToListenerName) + N'.' + QUOTENAME(@WriteBackupsToDatabaseName) + N'.dbo.backupset ' - SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, + SET @StringToExecute += N' (database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, time_zone, compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 THEN + N'encryptor_type, has_bulk_logged_data)' + @crlf @@ -1466,7 +1473,7 @@ END END SET @StringToExecute +=N' - SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, + SELECT database_name, database_guid, backup_set_uuid, type, backup_size, backup_start_date, backup_finish_date, media_set_id, time_zone, compressed_backup_size, recovery_model, server_name, machine_name, first_lsn, last_lsn, user_name, compatibility_level, is_password_protected, is_snapshot, is_readonly, is_single_user, has_backup_checksums, is_damaged, ' + CASE WHEN @ProductVersionMajor >= 12 THEN + N'encryptor_type, has_bulk_logged_data' + @crlf diff --git a/sp_BlitzCache.sql b/sp_BlitzCache.sql index d66a74f89..0df4e8a4e 100644 --- a/sp_BlitzCache.sql +++ b/sp_BlitzCache.sql @@ -27,28 +27,28 @@ IF OBJECT_ID('dbo.sp_BlitzCache') IS NULL EXEC ('CREATE PROCEDURE dbo.sp_BlitzCache AS RETURN 0;'); GO -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheProcs;'); +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheProcs', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheProcs;'); GO -IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults', 'U') IS NOT NULL - EXEC ('DROP TABLE ##bou_BlitzCacheResults;'); +IF OBJECT_ID('dbo.sp_BlitzCache') IS NOT NULL AND OBJECT_ID('tempdb.dbo.##BlitzCacheResults', 'U') IS NOT NULL + EXEC ('DROP TABLE ##BlitzCacheResults;'); GO -CREATE TABLE ##bou_BlitzCacheResults ( +CREATE TABLE ##BlitzCacheResults ( SPID INT, ID INT IDENTITY(1,1), CheckID INT, Priority TINYINT, FindingsGroup VARCHAR(50), - Finding VARCHAR(200), + Finding VARCHAR(500), URL VARCHAR(200), Details VARCHAR(4000) ); -CREATE TABLE ##bou_BlitzCacheProcs ( +CREATE TABLE ##BlitzCacheProcs ( SPID INT , - QueryType NVARCHAR(256), + QueryType NVARCHAR(258), DatabaseName sysname, AverageCPU DECIMAL(38,4), AverageCPUPerMinute DECIMAL(38,4), @@ -75,6 +75,7 @@ CREATE TABLE ##bou_BlitzCacheProcs ( PlanCreationTime DATETIME, PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), LastExecutionTime DATETIME, + LastCompletionTime DATETIME, PlanHandle VARBINARY(64), [Remove Plan Handle From Cache] AS CASE WHEN [PlanHandle] IS NOT NULL @@ -97,6 +98,7 @@ CREATE TABLE ##bou_BlitzCacheProcs ( QueryPlanHash BINARY(8), StatementStartOffset INT, StatementEndOffset INT, + PlanGenerationNum BIGINT, MinReturnedRows BIGINT, MaxReturnedRows BIGINT, AverageReturnedRows MONEY, @@ -111,6 +113,10 @@ CREATE TABLE ##bou_BlitzCacheProcs ( MaxUsedGrantKB BIGINT, PercentMemoryGrantUsed MONEY, AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, QueryText NVARCHAR(MAX), QueryPlan XML, /* these next four columns are the total for the type of query. @@ -118,9 +124,9 @@ CREATE TABLE ##bou_BlitzCacheProcs ( */ TotalWorkerTimeForType BIGINT, TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, + TotalReadsForType DECIMAL(30), TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, + TotalWritesForType DECIMAL(30), NumberOfPlans INT, NumberOfDistinctPlans INT, SerialDesiredMemory FLOAT, @@ -129,6 +135,7 @@ CREATE TABLE ##bou_BlitzCacheProcs ( CompileTime FLOAT, CompileCPU FLOAT , CompileMemory FLOAT , + MaxCompileMemory FLOAT , min_worker_time BIGINT, max_worker_time BIGINT, is_forced_plan BIT, @@ -136,6 +143,8 @@ CREATE TABLE ##bou_BlitzCacheProcs ( is_cursor BIT, is_optimistic_cursor BIT, is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, is_parallel BIT, is_forced_serial BIT, is_key_lookup_expensive BIT, @@ -147,7 +156,7 @@ CREATE TABLE ##bou_BlitzCacheProcs ( unparameterized_query BIT, near_parallel BIT, plan_warnings BIT, - plan_multiple_plans BIT, + plan_multiple_plans INT, long_running BIT, downlevel_estimator BIT, implicit_conversions BIT, @@ -205,16 +214,27 @@ CREATE TABLE ##bou_BlitzCacheProcs ( is_adaptive BIT, index_spool_cost FLOAT, index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, is_spool_expensive BIT, is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, estimated_rows FLOAT, is_bad_estimate BIT, is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, implicit_conversion_info XML, cached_execution_parameters XML, missing_indexes XML, SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) ); GO @@ -225,16 +245,18 @@ ALTER PROCEDURE dbo.sp_BlitzCache @UseTriggersAnyway BIT = NULL, @ExportToExcel BIT = 0, @ExpertMode TINYINT = 0, - @OutputServerName NVARCHAR(256) = NULL , - @OutputDatabaseName NVARCHAR(256) = NULL , - @OutputSchemaName NVARCHAR(256) = NULL , - @OutputTableName NVARCHAR(256) = NULL , + @OutputType VARCHAR(20) = 'TABLE' , + @OutputServerName NVARCHAR(258) = NULL , + @OutputDatabaseName NVARCHAR(258) = NULL , + @OutputSchemaName NVARCHAR(258) = NULL , + @OutputTableName NVARCHAR(258) = NULL , -- do NOT use ##BlitzCacheResults or ##BlitzCacheProcs as they are used as work tables in this procedure @ConfigurationDatabaseName NVARCHAR(128) = NULL , - @ConfigurationSchemaName NVARCHAR(256) = NULL , - @ConfigurationTableName NVARCHAR(256) = NULL , + @ConfigurationSchemaName NVARCHAR(258) = NULL , + @ConfigurationTableName NVARCHAR(258) = NULL , @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 , @@ -242,449 +264,508 @@ ALTER PROCEDURE dbo.sp_BlitzCache @QueryFilter VARCHAR(10) = 'ALL' , @DatabaseName NVARCHAR(128) = NULL , @StoredProcName NVARCHAR(128) = NULL, + @SlowlySearchPlansFor NVARCHAR(4000) = NULL, @Reanalyze BIT = 0 , @SkipAnalysis BIT = 0 , - @BringThePain BIT = 0, /* This will forcibly set @Top to 2,147,483,647 */ + @BringThePain BIT = 0 , @MinimumExecutionCount INT = 0, @Debug BIT = 0, @CheckDateOverride DATETIMEOFFSET = NULL, @MinutesBack INT = NULL, - @VersionDate DATETIME = NULL OUTPUT + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @KeepCRLF BIT = 0 WITH RECOMPILE AS BEGIN SET NOCOUNT ON; +SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; - -IF @Help = 1 PRINT ' -sp_BlitzCache from http://FirstResponderKit.org - -This script displays your most resource-intensive queries from the plan cache, -and points to ways you can tune these queries to make them faster. +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; -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. +DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; + +IF @Help = 1 + BEGIN + PRINT ' + sp_BlitzCache from http://FirstResponderKit.org + + This script displays your most resource-intensive queries from the plan cache, + and points to ways you can tune these queries to make them faster. -Known limitations of this version: - - This query will not run on SQL Server 2005. - - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is - excluded by default. - - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes - with no spaces between the hash values. - - @OutputServerName is not functional yet. -Unknown limitations of this version: - - May or may not be vulnerable to the wick effect. + 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. -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + Known limitations of this version: + - SQL Server 2008 and 2008R2 have a bug in trigger stats, so that output is + excluded by default. + - @IgnoreQueryHashes and @OnlyQueryHashes require a CSV list of hashes + with no spaces between the hash values. + Unknown limitations of this version: + - May or may not be vulnerable to the wick effect. + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ -MIT License -Copyright (c) 2016 Brent Ozar Unlimited -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: + MIT License -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. + Copyright (c) Brent Ozar Unlimited -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. -'; + 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: -DECLARE @nl NVARCHAR(2) = NCHAR(13) + NCHAR(10) ; + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. -IF @Help = 1 -BEGIN - SELECT N'@Help' AS [Parameter Name] , - N'BIT' AS [Data Type] , - N'Displays this help message.' AS [Parameter Description] + 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. + '; - UNION ALL - SELECT N'@Top', - N'INT', - N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' + + SELECT N'@Help' AS [Parameter Name] , + N'BIT' AS [Data Type] , + N'Displays this help message.' AS [Parameter Description] - UNION ALL - SELECT N'@SortOrder', - N'VARCHAR(10)', - N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' + UNION ALL + SELECT N'@Top', + N'INT', + N'The number of records to retrieve and analyze from the plan cache. The following DMVs are used as the plan cache: dm_exec_query_stats, dm_exec_procedure_stats, dm_exec_trigger_stats.' - UNION ALL - SELECT N'@UseTriggersAnyway', - N'BIT', - N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' + UNION ALL + SELECT N'@SortOrder', + N'VARCHAR(10)', + N'Data processing and display order. @SortOrder will still be used, even when preparing output for a table or for excel. Possible values are: "CPU", "Reads", "Writes", "Duration", "Executions", "Recent Compilations", "Memory Grant", "Unused Grant", "Spills", "Query Hash", "Duplicate". Additionally, the word "Average" or "Avg" can be used to sort on averages rather than total. "Executions per minute" and "Executions / minute" can be used to sort by execution per minute. For the truly lazy, "xpm" can also be used. Note that when you use all or all avg, the only parameters you can use are @Top and @DatabaseName. All others will be ignored.' - UNION ALL - SELECT N'@ExportToExcel', - N'BIT', - N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' + UNION ALL + SELECT N'@UseTriggersAnyway', + N'BIT', + N'On SQL Server 2008R2 and earlier, trigger execution count is incorrect - trigger execution count is incremented once per execution of a SQL agent job. If you still want to see relative execution count of triggers, then you can force sp_BlitzCache to include this information.' - UNION ALL - SELECT N'@ExpertMode', - N'TINYINT', - N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' + UNION ALL + SELECT N'@ExportToExcel', + N'BIT', + N'Prepare output for exporting to Excel. Newlines and additional whitespace are removed from query text and the execution plan is not displayed.' + UNION ALL + SELECT N'@ExpertMode', + N'TINYINT', + N'Default 0. When set to 1, results include more columns. When 2, mode is optimized for Opserver, the open source dashboard.' UNION ALL - SELECT N'@OutputDatabaseName', - N'NVARCHAR(128)', - N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' + SELECT N'@OutputType', + N'NVARCHAR(258)', + N'If set to "NONE", this will tell this procedure not to run any query leading to a results set thrown to caller.' + + UNION ALL + SELECT N'@OutputDatabaseName', + N'NVARCHAR(128)', + N'The output database. If this does not exist SQL Server will divide by zero and everything will fall apart.' - UNION ALL - SELECT N'@OutputSchemaName', - N'NVARCHAR(256)', - N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' + UNION ALL + SELECT N'@OutputSchemaName', + N'NVARCHAR(258)', + N'The output schema. If this does not exist SQL Server will divide by zero and everything will fall apart.' - UNION ALL - SELECT N'@OutputTableName', - N'NVARCHAR(256)', - N'The output table. If this does not exist, it will be created for you.' + UNION ALL + SELECT N'@OutputTableName', + N'NVARCHAR(258)', + N'The output table. If this does not exist, it will be created for you.' - UNION ALL - SELECT N'@DurationFilter', - N'DECIMAL(38,4)', - N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' + UNION ALL + SELECT N'@DurationFilter', + N'DECIMAL(38,4)', + N'Excludes queries with an average duration (in seconds) less than @DurationFilter.' - UNION ALL - SELECT N'@HideSummary', - N'BIT', - N'Hides the findings summary result set.' + UNION ALL + SELECT N'@HideSummary', + N'BIT', + N'Hides the findings summary result set.' - UNION ALL - SELECT N'@IgnoreSystemDBs', - N'BIT', - N'Ignores plans found in the system databases (master, model, msdb, tempdb, and resourcedb)' + UNION ALL + SELECT N'@IgnoreSystemDBs', + N'BIT', + N'Ignores plans found in the system databases (master, model, msdb, tempdb, dbmaintenance, dbadmin, dbatools and resourcedb)' - UNION ALL - SELECT N'@OnlyQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' + UNION ALL + SELECT N'@OnlyQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to query. All other query hashes will be ignored. Stored procedures and triggers will be ignored.' - UNION ALL - SELECT N'@IgnoreQueryHashes', - N'VARCHAR(MAX)', - N'A list of query hashes to ignore.' + UNION ALL + SELECT N'@IgnoreQueryHashes', + N'VARCHAR(MAX)', + N'A list of query hashes to ignore.' - UNION ALL - SELECT N'@OnlySqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to use for filtering results.' + UNION ALL + SELECT N'@OnlySqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to use for filtering results.' UNION ALL - SELECT N'@IgnoreSqlHandles', - N'VARCHAR(MAX)', - N'One or more sql_handles to ignore.' + SELECT N'@IgnoreSqlHandles', + N'VARCHAR(MAX)', + N'One or more sql_handles to ignore.' - UNION ALL - SELECT N'@DatabaseName', - N'NVARCHAR(128)', - N'A database name which is used for filtering results.' + UNION ALL + SELECT N'@DatabaseName', + N'NVARCHAR(128)', + N'A database name which is used for filtering results.' - UNION ALL - SELECT N'@StoredProcName', - N'NVARCHAR(128)', - N'Name of stored procedure you want to find plans for.' + UNION ALL + SELECT N'@StoredProcName', + N'NVARCHAR(128)', + N'Name of stored procedure you want to find plans for.' - UNION ALL - SELECT N'@BringThePain', - N'BIT', - N'This forces sp_BlitzCache to examine the entire plan cache. Be careful running this on servers with a lot of memory or a large execution plan cache.' + UNION ALL + SELECT N'@SlowlySearchPlansFor', + N'NVARCHAR(4000)', + N'String to search for in plan text. % wildcards allowed.' - UNION ALL - SELECT N'@QueryFilter', - N'VARCHAR(10)', - N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' + UNION ALL + SELECT N'@BringThePain', + N'BIT', + N'When using @SortOrder = ''all'' and @Top > 10, we require you to set @BringThePain = 1 so you understand that sp_BlitzCache will take a while to run.' - UNION ALL - SELECT N'@Reanalyze', - N'BIT', - N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' + UNION ALL + SELECT N'@QueryFilter', + N'VARCHAR(10)', + N'Filter out stored procedures or statements. The default value is ''ALL''. Allowed values are ''procedures'', ''statements'', ''functions'', or ''all'' (any variation in capitalization is acceptable).' + + UNION ALL + SELECT N'@Reanalyze', + N'BIT', + N'The default is 0. When set to 0, sp_BlitzCache will re-evalute the plan cache. Set this to 1 to reanalyze existing results' - UNION ALL - SELECT N'@MinimumExecutionCount', - N'INT', - N'Queries with fewer than this number of executions will be omitted from results.' + UNION ALL + SELECT N'@MinimumExecutionCount', + N'INT', + N'Queries with fewer than this number of executions will be omitted from results.' UNION ALL - SELECT N'@Debug', - N'BIT', - N'Setting this to 1 will print dynamic SQL and select data from all tables used.' + SELECT N'@Debug', + N'BIT', + N'Setting this to 1 will print dynamic SQL and select data from all tables used.' - UNION ALL - SELECT N'@MinutesBack', - N'INT', - N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negtive.'; + UNION ALL + SELECT N'@MinutesBack', + N'INT', + N'How many minutes back to begin plan cache analysis. If you put in a positive number, we''ll flip it to negative.' + UNION ALL + SELECT N'@Version', + N'VARCHAR(30)', + N'OUTPUT parameter holding version number.' + + UNION ALL + SELECT N'@VersionDate', + N'DATETIME', + N'OUTPUT parameter holding version date.' - /* Column definitions */ - SELECT N'# Executions' AS [Column Name], - N'BIGINT' AS [Data Type], - N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] + UNION ALL + SELECT N'@VersionCheckMode', + N'BIT', + N'Setting this to 1 will make the procedure stop after setting @Version and @VersionDate.' - UNION ALL - SELECT N'Executions / Minute', - N'MONEY', - N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' + UNION ALL + SELECT N'@KeepCRLF', + N'BIT', + N'Retain CR/LF in query text to avoid issues caused by line comments.'; - UNION ALL - SELECT N'Execution Weight', - N'MONEY', - N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'Database', - N'sysname', - N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' + /* Column definitions */ + SELECT N'# Executions' AS [Column Name], + N'BIGINT' AS [Data Type], + N'The number of executions of this particular query. This is computed across statements, procedures, and triggers and aggregated by the SQL handle.' AS [Column Description] - UNION ALL - SELECT N'Total CPU', - N'BIGINT', - N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' + UNION ALL + SELECT N'Executions / Minute', + N'MONEY', + N'Number of executions per minute - calculated for the life of the current plan. Plan life is the last execution time minus the plan creation time.' - UNION ALL - SELECT N'Avg CPU', - N'BIGINT', - N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' + UNION ALL + SELECT N'Execution Weight', + N'MONEY', + N'An arbitrary metric of total "execution-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'CPU Weight', - N'MONEY', - N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' + UNION ALL + SELECT N'Database', + N'sysname', + N'The name of the database where the plan was encountered. If the database name cannot be determined for some reason, a value of NA will be substituted. A value of 32767 indicates the plan comes from ResourceDB.' - UNION ALL - SELECT N'Total Duration', - N'BIGINT', - N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' + UNION ALL + SELECT N'Total CPU', + N'BIGINT', + N'Total CPU time, reported in milliseconds, that was consumed by all executions of this query since the last compilation.' - UNION ALL - SELECT N'Avg Duration', - N'BIGINT', - N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' + UNION ALL + SELECT N'Avg CPU', + N'BIGINT', + N'Average CPU time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - UNION ALL - SELECT N'Duration Weight', - N'MONEY', - N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' + UNION ALL + SELECT N'CPU Weight', + N'MONEY', + N'An arbitrary metric of total "CPU-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'Total Reads', - N'BIGINT', - N'Total logical reads performed by this query since last compilation.' + UNION ALL + SELECT N'Total Duration', + N'BIGINT', + N'Total elapsed time, reported in milliseconds, consumed by all executions of this query since last compilation.' - UNION ALL - SELECT N'Average Reads', - N'BIGINT', - N'Average logical reads performed by each execution of this query since the last compilation.' + UNION ALL + SELECT N'Avg Duration', + N'BIGINT', + N'Average elapsed time, reported in milliseconds, consumed by each execution of this query since the last compilation.' - UNION ALL - SELECT N'Read Weight', - N'MONEY', - N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' + UNION ALL + SELECT N'Duration Weight', + N'MONEY', + N'An arbitrary metric of total "Duration-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'Total Writes', - N'BIGINT', - N'Total logical writes performed by this query since last compilation.' + UNION ALL + SELECT N'Total Reads', + N'BIGINT', + N'Total logical reads performed by this query since last compilation.' - UNION ALL - SELECT N'Average Writes', - N'BIGINT', - N'Average logical writes performed by each execution this query since last compilation.' + UNION ALL + SELECT N'Average Reads', + N'BIGINT', + N'Average logical reads performed by each execution of this query since the last compilation.' - UNION ALL - SELECT N'Write Weight', - N'MONEY', - N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' + UNION ALL + SELECT N'Read Weight', + N'MONEY', + N'An arbitrary metric of "Read-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'Query Type', - N'NVARCHAR(256)', - N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' + UNION ALL + SELECT N'Total Writes', + N'BIGINT', + N'Total logical writes performed by this query since last compilation.' - UNION ALL - SELECT N'Query Text', - N'NVARCHAR(4000)', - N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' + UNION ALL + SELECT N'Average Writes', + N'BIGINT', + N'Average logical writes performed by each execution this query since last compilation.' - UNION ALL - SELECT N'% Executions (Type)', - N'MONEY', - N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' + UNION ALL + SELECT N'Write Weight', + N'MONEY', + N'An arbitrary metric of "Write-ness". A weight of 2 is "one more" than a weight of 1.' - UNION ALL - SELECT N'% CPU (Type)', - N'MONEY', - N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' + UNION ALL + SELECT N'Query Type', + N'NVARCHAR(258)', + N'The type of query being examined. This can be "Procedure", "Statement", or "Trigger".' - UNION ALL - SELECT N'% Duration (Type)', - N'MONEY', - N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' + UNION ALL + SELECT N'Query Text', + N'NVARCHAR(4000)', + N'The text of the query. This may be truncated by either SQL Server or by sp_BlitzCache(tm) for display purposes.' - UNION ALL - SELECT N'% Reads (Type)', - N'MONEY', - N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' + UNION ALL + SELECT N'% Executions (Type)', + N'MONEY', + N'Percent of executions relative to the type of query - e.g. 17.2% of all stored procedure executions.' - UNION ALL - SELECT N'% Writes (Type)', - N'MONEY', - N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' + UNION ALL + SELECT N'% CPU (Type)', + N'MONEY', + N'Percent of CPU time consumed by this query for a given type of query - e.g. 22% of CPU of all stored procedures executed.' - UNION ALL - SELECT N'Total Rows', - N'BIGINT', - N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' + UNION ALL + SELECT N'% Duration (Type)', + N'MONEY', + N'Percent of elapsed time consumed by this query for a given type of query - e.g. 12% of all statements executed.' - UNION ALL - SELECT N'Average Rows', - N'MONEY', - N'Average number of rows returned by each execution of the query.' + UNION ALL + SELECT N'% Reads (Type)', + N'MONEY', + N'Percent of reads consumed by this query for a given type of query - e.g. 34.2% of all stored procedures executed.' - UNION ALL - SELECT N'Min Rows', - N'BIGINT', - N'The minimum number of rows returned by any execution of this query.' + UNION ALL + SELECT N'% Writes (Type)', + N'MONEY', + N'Percent of writes performed by this query for a given type of query - e.g. 43.2% of all statements executed.' - UNION ALL - SELECT N'Max Rows', - N'BIGINT', - N'The maximum number of rows returned by any execution of this query.' + UNION ALL + SELECT N'Total Rows', + N'BIGINT', + N'Total number of rows returned for all executions of this query. This only applies to query level stats, not stored procedures or triggers.' - UNION ALL - SELECT N'MinGrantKB', - N'BIGINT', - N'The minimum memory grant the query received in kb.' + UNION ALL + SELECT N'Average Rows', + N'MONEY', + N'Average number of rows returned by each execution of the query.' - UNION ALL - SELECT N'MaxGrantKB', - N'BIGINT', - N'The maximum memory grant the query received in kb.' + UNION ALL + SELECT N'Min Rows', + N'BIGINT', + N'The minimum number of rows returned by any execution of this query.' - UNION ALL - SELECT N'MinUsedGrantKB', - N'BIGINT', - N'The minimum used memory grant the query received in kb.' + UNION ALL + SELECT N'Max Rows', + N'BIGINT', + N'The maximum number of rows returned by any execution of this query.' - UNION ALL - SELECT N'MaxUsedGrantKB', - N'BIGINT', - N'The maximum used memory grant the query received in kb.' + UNION ALL + SELECT N'MinGrantKB', + N'BIGINT', + N'The minimum memory grant the query received in kb.' - UNION ALL - SELECT N'PercentMemoryGrantUsed', - N'MONEY', - N'Result of dividing the maximum grant used by the minimum granted.' + UNION ALL + SELECT N'MaxGrantKB', + N'BIGINT', + N'The maximum memory grant the query received in kb.' - UNION ALL - SELECT N'AvgMaxMemoryGrant', - N'MONEY', - N'The average maximum memory grant for a query.' + UNION ALL + SELECT N'MinUsedGrantKB', + N'BIGINT', + N'The minimum used memory grant the query received in kb.' - UNION ALL - SELECT N'# Plans', - N'INT', - N'The total number of execution plans found that match a given query.' + UNION ALL + SELECT N'MaxUsedGrantKB', + N'BIGINT', + N'The maximum used memory grant the query received in kb.' - UNION ALL - SELECT N'# Distinct Plans', - N'INT', - N'The number of distinct execution plans that match a given query. ' - + NCHAR(13) + NCHAR(10) - + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' + UNION ALL + SELECT N'MinSpills', + N'BIGINT', + N'The minimum amount this query has spilled to tempdb in 8k pages.' - UNION ALL - SELECT N'Created At', - N'DATETIME', - N'Time that the execution plan was last compiled.' + UNION ALL + SELECT N'MaxSpills', + N'BIGINT', + N'The maximum amount this query has spilled to tempdb in 8k pages.' - UNION ALL - SELECT N'Last Execution', - N'DATETIME', - N'The last time that this query was executed.' + UNION ALL + SELECT N'TotalSpills', + N'BIGINT', + N'The total amount this query has spilled to tempdb in 8k pages.' - UNION ALL - SELECT N'Query Plan', - N'XML', - N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' + UNION ALL + SELECT N'AvgSpills', + N'BIGINT', + N'The average amount this query has spilled to tempdb in 8k pages.' - UNION ALL - SELECT N'Plan Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to the compiled plan this query is a part of.' + UNION ALL + SELECT N'PercentMemoryGrantUsed', + N'MONEY', + N'Result of dividing the maximum grant used by the minimum granted.' - UNION ALL - SELECT N'SQL Handle', - N'VARBINARY(64)', - N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' + UNION ALL + SELECT N'AvgMaxMemoryGrant', + N'MONEY', + N'The average maximum memory grant for a query.' - UNION ALL - SELECT N'Query Hash', - N'BINARY(8)', - N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' + UNION ALL + SELECT N'# Plans', + N'INT', + N'The total number of execution plans found that match a given query.' - UNION ALL - SELECT N'Warnings', - N'VARCHAR(MAX)', - N'A list of individual warnings generated by this query.' ; + UNION ALL + SELECT N'# Distinct Plans', + N'INT', + N'The number of distinct execution plans that match a given query. ' + + NCHAR(13) + NCHAR(10) + + N'This may be caused by running the same query across multiple databases or because of a lack of proper parameterization in the database.' + + UNION ALL + SELECT N'Created At', + N'DATETIME', + N'Time that the execution plan was last compiled.' + + UNION ALL + SELECT N'Last Execution', + N'DATETIME', + N'The last time that this query was executed.' + + UNION ALL + SELECT N'Query Plan', + N'XML', + N'The query plan. Click to display a graphical plan or, if you need to patch SSMS, a pile of XML.' + + UNION ALL + SELECT N'Plan Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to the compiled plan this query is a part of.' + + UNION ALL + SELECT N'SQL Handle', + N'VARBINARY(64)', + N'An arbitrary identifier referring to a batch or stored procedure that this query is a part of.' + + UNION ALL + SELECT N'Query Hash', + N'BINARY(8)', + N'A hash of the query. Queries with the same query hash have similar logic but only differ by literal values or database.' + + UNION ALL + SELECT N'Warnings', + N'VARCHAR(MAX)', + N'A list of individual warnings generated by this query.' ; - /* Configuration table description */ - SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , - N'100' AS [Default Value] , - N'Executions / Minute' AS [Unit of Measure] , - N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] + /* Configuration table description */ + SELECT N'Frequent Execution Threshold' AS [Configuration Parameter] , + N'100' AS [Default Value] , + N'Executions / Minute' AS [Unit of Measure] , + N'Executions / Minute before a "Frequent Execution Threshold" warning is triggered.' AS [Description] - UNION ALL - SELECT N'Parameter Sniffing Variance Percent' , - N'30' , - N'Percent' , - N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' + UNION ALL + SELECT N'Parameter Sniffing Variance Percent' , + N'30' , + N'Percent' , + N'Variance required between min/max values and average values before a "Parameter Sniffing" warning is triggered. Applies to worker time and returned rows.' - UNION ALL - SELECT N'Parameter Sniffing IO Threshold' , - N'100,000' , - N'Logical reads' , - N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' + UNION ALL + SELECT N'Parameter Sniffing IO Threshold' , + N'100,000' , + N'Logical reads' , + N'Minimum number of average logical reads before parameter sniffing checks are evaluated.' - UNION ALL - SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' + UNION ALL + SELECT N'Cost Threshold for Parallelism Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Trigger a "Nearly Parallel" warning when a query''s cost is within X percent of the cost threshold for parallelism.' + + UNION ALL + SELECT N'Long Running Query Warning' AS [Configuration Parameter] , + N'300' , + N'Seconds' , + N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' + + UNION ALL + SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , + N'10' , + N'Percent' , + N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; + RETURN; + END; /* IF @Help = 1 */ - UNION ALL - SELECT N'Long Running Query Warning' AS [Configuration Parameter] , - N'300' , - N'Seconds' , - N'Triggers a "Long Running Query Warning" when average duration, max CPU time, or max clock time is higher than this number.' - UNION ALL - SELECT N'Unused Memory Grant Warning' AS [Configuration Parameter] , - N'10' , - N'Percent' , - N'Triggers an "Unused Memory Grant Warning" when a query uses >= X percent of its memory grant.'; - RETURN; -END; /*Validate version*/ IF ( @@ -702,10 +783,25 @@ BEGIN RETURN; END; +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; +END; + +IF(@OutputType = 'NONE') +BEGIN + SET @HideSummary = 1; +END; + + +/* Lets get @SortOrder set to lower case here for comparisons later */ +SET @SortOrder = LOWER(@SortOrder); + /* Set @Top based on sort */ IF ( @Top IS NULL - AND LOWER(@SortOrder) IN ( 'all', 'all sort' ) + AND @SortOrder IN ( 'all', 'all sort' ) ) BEGIN SET @Top = 5; @@ -713,12 +809,74 @@ IF ( IF ( @Top IS NULL - AND LOWER(@SortOrder) NOT IN ( 'all', 'all sort' ) + AND @SortOrder NOT IN ( 'all', 'all sort' ) ) BEGIN SET @Top = 10; END; + +/* If they want to sort by query hash, populate the @OnlyQueryHashes list for them */ +IF @SortOrder LIKE 'query hash%' + BEGIN + RAISERROR('Beginning query hash sort', 0, 1) WITH NOWAIT; + + SELECT TOP(@Top) qs.query_hash, + MAX(qs.max_worker_time) AS max_worker_time, + COUNT_BIG(*) AS records + INTO #query_hash_grouped + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY ( SELECT pa.value + FROM sys.dm_exec_plan_attributes(qs.plan_handle) AS pa + WHERE pa.attribute = 'dbid' ) AS ca + GROUP BY qs.query_hash, ca.value + HAVING COUNT_BIG(*) > 1 + ORDER BY max_worker_time DESC, + records DESC; + + SELECT TOP (1) + @OnlyQueryHashes = STUFF((SELECT DISTINCT N',' + CONVERT(NVARCHAR(MAX), qhg.query_hash, 1) + FROM #query_hash_grouped AS qhg + WHERE qhg.query_hash <> 0x00 + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + OPTION(RECOMPILE); + + /* When they ran it, @SortOrder probably looked like 'query hash, cpu', so strip the first sort order out: */ + SELECT @SortOrder = LTRIM(REPLACE(REPLACE(@SortOrder,'query hash', ''), ',', '')); + + /* If they just called it with @SortOrder = 'query hash', set it to 'cpu' for backwards compatibility: */ + IF @SortOrder = '' SET @SortOrder = 'cpu'; + + END + + +/* If they want to sort by duplicate, create a table with the worst offenders - issue #3345 */ +IF @SortOrder LIKE 'duplicate%' + BEGIN + RAISERROR('Beginning duplicate query hash sort', 0, 1) WITH NOWAIT; + + /* Find the query hashes that are the most duplicated */ + WITH MostCommonQueries AS ( + SELECT TOP(@Top) qs.query_hash, + COUNT_BIG(*) AS plans + FROM sys.dm_exec_query_stats AS qs + GROUP BY qs.query_hash + HAVING COUNT_BIG(*) > 100 + ORDER BY COUNT_BIG(*) DESC + ) + SELECT mcq_recent.sql_handle, mcq_recent.plan_handle, mcq_recent.creation_time AS duplicate_creation_time, mcq.plans + INTO #duplicate_query_filter + FROM MostCommonQueries mcq + CROSS APPLY ( SELECT TOP 1 qs.sql_handle, qs.plan_handle, qs.creation_time + FROM sys.dm_exec_query_stats qs + WHERE qs.query_hash = mcq.query_hash + ORDER BY qs.creation_time DESC) AS mcq_recent + OPTION (RECOMPILE); + + SET @MinimumExecutionCount = 0; + END + + /* validate user inputs */ IF @Top IS NULL OR @SortOrder IS NULL @@ -745,219 +903,53 @@ IF @MinutesBack IS NOT NULL END; -RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheResults') IS NULL +DECLARE @DurationFilter_i INT, + @MinMemoryPerQuery INT, + @msg NVARCHAR(4000), + @NoobSaibot BIT = 0, + @VersionShowsAirQuoteActualPlans BIT, + @ObjectFullName NVARCHAR(2000), + @user_perm_sql NVARCHAR(MAX) = N'', + @user_perm_gb_out DECIMAL(10,2), + @common_version DECIMAL(10,2), + @buffer_pool_memory_gb DECIMAL(10,2), + @user_perm_percent DECIMAL(10,2), + @is_tokenstore_big BIT = 0, + @sort NVARCHAR(MAX) = N'', + @sort_filter NVARCHAR(MAX) = N''; + + +IF @SortOrder = 'sp_BlitzIndex' BEGIN - CREATE TABLE ##bou_BlitzCacheResults ( - SPID INT, - ID INT IDENTITY(1,1), - CheckID INT, - Priority TINYINT, - FindingsGroup VARCHAR(50), - Finding VARCHAR(200), - URL VARCHAR(200), - Details VARCHAR(4000) - ); -END; + RAISERROR(N'OUTSTANDING!', 0, 1) WITH NOWAIT; + SET @SortOrder = 'reads'; + SET @NoobSaibot = 1; + +END + + +/* Change duration from seconds to milliseconds */ +IF @DurationFilter IS NOT NULL + BEGIN + RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; + SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); + END; + +RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; +SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; -IF OBJECT_ID('tempdb.dbo.##bou_BlitzCacheProcs') IS NULL +IF SERVERPROPERTY('EngineEdition') IN (5, 6) AND DB_NAME() <> @DatabaseName BEGIN - CREATE TABLE ##bou_BlitzCacheProcs ( - SPID INT , - QueryType NVARCHAR(256), - DatabaseName sysname, - AverageCPU DECIMAL(38,4), - AverageCPUPerMinute DECIMAL(38,4), - TotalCPU DECIMAL(38,4), - PercentCPUByType MONEY, - PercentCPU MONEY, - AverageDuration DECIMAL(38,4), - TotalDuration DECIMAL(38,4), - PercentDuration MONEY, - PercentDurationByType MONEY, - AverageReads BIGINT, - TotalReads BIGINT, - PercentReads MONEY, - PercentReadsByType MONEY, - ExecutionCount BIGINT, - PercentExecutions MONEY, - PercentExecutionsByType MONEY, - ExecutionsPerMinute MONEY, - TotalWrites BIGINT, - AverageWrites MONEY, - PercentWrites MONEY, - PercentWritesByType MONEY, - WritesPerMinute MONEY, - PlanCreationTime DATETIME, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime DATETIME, - PlanHandle VARBINARY(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' - ELSE 'N/A' END, - SqlHandle VARBINARY(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' - ELSE 'N/A' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' - ELSE 'N/A' END, - QueryHash BINARY(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' - ELSE 'N/A' END, - QueryPlanHash BINARY(8), - StatementStartOffset INT, - StatementEndOffset INT, - MinReturnedRows BIGINT, - MaxReturnedRows BIGINT, - AverageReturnedRows MONEY, - TotalReturnedRows BIGINT, - LastReturnedRows BIGINT, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryText NVARCHAR(MAX), - QueryPlan XML, - /* these next four columns are the total for the type of query. - don't actually use them for anything apart from math by type. - */ - TotalWorkerTimeForType BIGINT, - TotalElapsedTimeForType BIGINT, - TotalReadsForType BIGINT, - TotalExecutionCountForType BIGINT, - TotalWritesForType BIGINT, - NumberOfPlans INT, - NumberOfDistinctPlans INT, - SerialDesiredMemory FLOAT, - SerialRequiredMemory FLOAT, - CachedPlanSize FLOAT, - CompileTime FLOAT, - CompileCPU FLOAT , - CompileMemory FLOAT , - min_worker_time BIGINT, - max_worker_time BIGINT, - is_forced_plan BIT, - is_forced_parameterized BIT, - is_cursor BIT, - is_optimistic_cursor BIT, - is_forward_only_cursor BIT, - is_parallel BIT, - is_forced_serial BIT, - is_key_lookup_expensive BIT, - key_lookup_cost FLOAT, - is_remote_query_expensive BIT, - remote_query_cost FLOAT, - frequent_execution BIT, - parameter_sniffing BIT, - unparameterized_query BIT, - near_parallel BIT, - plan_warnings BIT, - plan_multiple_plans BIT, - long_running BIT, - downlevel_estimator BIT, - implicit_conversions BIT, - busy_loops BIT, - tvf_join BIT, - tvf_estimate BIT, - compile_timeout BIT, - compile_memory_limit_exceeded BIT, - warning_no_join_predicate BIT, - QueryPlanCost FLOAT, - missing_index_count INT, - unmatched_index_count INT, - min_elapsed_time BIGINT, - max_elapsed_time BIGINT, - age_minutes MONEY, - age_minutes_lifetime MONEY, - is_trivial BIT, - trace_flags_session VARCHAR(1000), - is_unused_grant BIT, - function_count INT, - clr_function_count INT, - is_table_variable BIT, - no_stats_warning BIT, - relop_warnings BIT, - is_table_scan BIT, - backwards_scan BIT, - forced_index BIT, - forced_seek BIT, - forced_scan BIT, - columnstore_row_mode BIT, - is_computed_scalar BIT , - is_sort_expensive BIT, - sort_cost FLOAT, - is_computed_filter BIT, - op_name VARCHAR(100) NULL, - index_insert_count INT NULL, - index_update_count INT NULL, - index_delete_count INT NULL, - cx_insert_count INT NULL, - cx_update_count INT NULL, - cx_delete_count INT NULL, - table_insert_count INT NULL, - table_update_count INT NULL, - table_delete_count INT NULL, - index_ops AS (index_insert_count + index_update_count + index_delete_count + - cx_insert_count + cx_update_count + cx_delete_count + - table_insert_count + table_update_count + table_delete_count), - is_row_level BIT, - is_spatial BIT, - index_dml BIT, - table_dml BIT, - long_running_low_cpu BIT, - low_cost_high_cpu BIT, - stale_stats BIT, - is_adaptive BIT, - index_spool_cost FLOAT, - index_spool_rows FLOAT, - is_spool_expensive BIT, - is_spool_more_rows BIT, - estimated_rows FLOAT, - is_bad_estimate BIT, - is_paul_white_electric BIT, - implicit_conversion_info XML, - cached_execution_parameters XML, - missing_indexes XML, - SetOptions VARCHAR(MAX), - Warnings VARCHAR(MAX) - ); + RAISERROR('You specified a database name other than the current database, but Azure SQL DB does not allow you to change databases. Execute sp_BlitzCache from the database you want to analyze.', 16, 1); + RETURN; END; - -DECLARE @DurationFilter_i INT, - @MinMemoryPerQuery INT, - @msg NVARCHAR(4000) ; - - -IF @BringThePain = 1 - BEGIN - RAISERROR(N'You have chosen to bring the pain. Setting top to 2147483647.', 0, 1) WITH NOWAIT; - SET @Top = 2147483647; - END; - -/* Change duration from seconds to milliseconds */ -IF @DurationFilter IS NOT NULL - BEGIN - RAISERROR(N'Converting Duration Filter to milliseconds', 0, 1) WITH NOWAIT; - SET @DurationFilter_i = CAST((@DurationFilter * 1000.0) AS INT); - END; - -RAISERROR(N'Checking database validity', 0, 1) WITH NOWAIT; -SET @DatabaseName = LTRIM(RTRIM(@DatabaseName)) ; -IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> '' +IF (DB_ID(@DatabaseName)) IS NULL AND @DatabaseName <> N'' BEGIN RAISERROR('The database you specified does not exist. Please check the name and try again.', 16, 1); RETURN; END; -IF (SELECT DATABASEPROPERTYEX(@DatabaseName, 'Status')) <> 'ONLINE' +IF (SELECT DATABASEPROPERTYEX(ISNULL(@DatabaseName, 'master'), 'Collation')) IS NULL AND SERVERPROPERTY('EngineEdition') NOT IN (5, 6, 8) BEGIN RAISERROR('The database you specified is not readable. Please check the name and try again. Better yet, check your server.', 16, 1); RETURN; @@ -965,27 +957,35 @@ END; SELECT @MinMemoryPerQuery = CONVERT(INT, c.value) FROM sys.configurations AS c WHERE c.name = 'min memory per query (KB)'; -SET @SortOrder = LOWER(@SortOrder); SET @SortOrder = REPLACE(REPLACE(@SortOrder, 'average', 'avg'), '.', ''); -SET @SortOrder = REPLACE(@SortOrder, 'executions per minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'executions / minute', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'xpm', 'avg executions'); -SET @SortOrder = REPLACE(@SortOrder, 'recent compilations', 'compiles'); +SET @SortOrder = CASE + WHEN @SortOrder IN ('executions per minute','execution per minute','executions / minute','execution / minute','xpm') THEN 'avg executions' + WHEN @SortOrder IN ('recent compilations','recent compilation','compile') THEN 'compiles' + WHEN @SortOrder IN ('read') THEN 'reads' + WHEN @SortOrder IN ('avg read') THEN 'avg reads' + WHEN @SortOrder IN ('write') THEN 'writes' + WHEN @SortOrder IN ('avg write') THEN 'avg writes' + WHEN @SortOrder IN ('memory grants') THEN 'memory grant' + WHEN @SortOrder IN ('avg memory grants') THEN 'avg memory grant' + WHEN @SortOrder IN ('unused grants','unused memory', 'unused memory grant', 'unused memory grants') THEN 'unused grant' + WHEN @SortOrder IN ('spill') THEN 'spills' + WHEN @SortOrder IN ('avg spill') THEN 'avg spills' + WHEN @SortOrder IN ('execution') THEN 'executions' + WHEN @SortOrder IN ('duplicates') THEN 'duplicate' + ELSE @SortOrder END + RAISERROR(N'Checking sort order', 0, 1) WITH NOWAIT; IF @SortOrder NOT IN ('cpu', 'avg cpu', 'reads', 'avg reads', 'writes', 'avg writes', 'duration', 'avg duration', 'executions', 'avg executions', - 'compiles', 'memory grant', 'avg memory grant', - 'all', 'all avg') + 'compiles', 'memory grant', 'avg memory grant', 'unused grant', + 'spills', 'avg spills', 'all', 'all avg', 'sp_BlitzIndex', + 'query hash', 'duplicate') BEGIN - RAISERROR(N'Invalid sort order chosen, reverting to cpu', 0, 1) WITH NOWAIT; + RAISERROR(N'Invalid sort order chosen, reverting to cpu', 16, 1) WITH NOWAIT; SET @SortOrder = 'cpu'; END; -SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), - @OutputSchemaName = QUOTENAME(@OutputSchemaName), - @OutputTableName = QUOTENAME(@OutputTableName); - SET @QueryFilter = LOWER(@QueryFilter); IF LEFT(@QueryFilter, 3) NOT IN ('all', 'sta', 'pro', 'fun') @@ -1000,29 +1000,38 @@ IF @SkipAnalysis = 1 SET @HideSummary = 1; END; -IF @Reanalyze = 1 AND OBJECT_ID('tempdb..##bou_BlitzCacheResults') IS NULL - BEGIN - RAISERROR(N'##bou_BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; - SET @Reanalyze = 0; - END; +DECLARE @AllSortSql NVARCHAR(MAX) = N''; +DECLARE @VersionShowsMemoryGrants BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_grant_kb') + SET @VersionShowsMemoryGrants = 1; +ELSE + SET @VersionShowsMemoryGrants = 0; -IF @Reanalyze = 0 - BEGIN - RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheResults - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; - DELETE ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - OPTION (RECOMPILE) ; - END; +DECLARE @VersionShowsSpills BIT; +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_stats') AND name = 'max_spills') + SET @VersionShowsSpills = 1; +ELSE + SET @VersionShowsSpills = 0; + +IF EXISTS(SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_plan_stats') AND name = 'query_plan') + SET @VersionShowsAirQuoteActualPlans = 1; +ELSE + SET @VersionShowsAirQuoteActualPlans = 0; IF @Reanalyze = 1 + BEGIN + IF OBJECT_ID('tempdb..##BlitzCacheResults') IS NULL + BEGIN + RAISERROR(N'##BlitzCacheResults does not exist, can''t reanalyze', 0, 1) WITH NOWAIT; + SET @Reanalyze = 0; + END + ELSE BEGIN RAISERROR(N'Reanalyzing current data, skipping to results', 0, 1) WITH NOWAIT; GOTO Results; END; + END; + IF @SortOrder IN ('all', 'all avg') BEGIN @@ -1030,7 +1039,6 @@ IF @SortOrder IN ('all', 'all avg') GOTO AllSorts; END; - RAISERROR(N'Creating temp tables for internal processing', 0, 1) WITH NOWAIT; IF OBJECT_ID('tempdb..#only_query_hashes') IS NOT NULL DROP TABLE #only_query_hashes ; @@ -1075,27 +1083,34 @@ IF OBJECT_ID ('tempdb..#trace_flags') IS NOT NULL DROP TABLE #trace_flags; IF OBJECT_ID('tempdb..#variable_info') IS NOT NULL - DROP TABLE #variable_info + DROP TABLE #variable_info; IF OBJECT_ID('tempdb..#conversion_info') IS NOT NULL - DROP TABLE #conversion_info - + DROP TABLE #conversion_info; IF OBJECT_ID('tempdb..#missing_index_xml') IS NOT NULL - DROP TABLE #missing_index_xml + DROP TABLE #missing_index_xml; IF OBJECT_ID('tempdb..#missing_index_schema') IS NOT NULL - DROP TABLE #missing_index_schema + DROP TABLE #missing_index_schema; IF OBJECT_ID('tempdb..#missing_index_usage') IS NOT NULL - DROP TABLE #missing_index_usage + DROP TABLE #missing_index_usage; IF OBJECT_ID('tempdb..#missing_index_detail') IS NOT NULL - DROP TABLE #missing_index_detail + DROP TABLE #missing_index_detail; IF OBJECT_ID('tempdb..#missing_index_pretty') IS NOT NULL - DROP TABLE #missing_index_pretty + DROP TABLE #missing_index_pretty; +IF OBJECT_ID('tempdb..#index_spool_ugly') IS NOT NULL + DROP TABLE #index_spool_ugly; + +IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + +IF OBJECT_ID('tempdb..#plan_usage') IS NOT NULL + DROP TABLE #plan_usage; CREATE TABLE #only_query_hashes ( query_hash BINARY(8) @@ -1155,6 +1170,7 @@ CREATE TABLE #plan_cost ( QueryPlanCost FLOAT, SqlHandle VARBINARY(64), + PlanHandle VARBINARY(64), QueryHash BINARY(8), QueryPlanHash BINARY(8) ); @@ -1170,12 +1186,12 @@ CREATE TABLE #stats_agg ( SqlHandle VARBINARY(64), LastUpdate DATETIME2(7), - ModificationCount INT, + ModificationCount BIGINT, SamplingPercent FLOAT, - [Statistics] NVARCHAR(256), - [Table] NVARCHAR(256), - [Schema] NVARCHAR(256), - [Database] NVARCHAR(256), + [Statistics] NVARCHAR(258), + [Table] NVARCHAR(258), + [Schema] NVARCHAR(258), + [Database] NVARCHAR(258), ); CREATE TABLE #trace_flags @@ -1191,13 +1207,14 @@ CREATE TABLE #stored_proc_info SPID INT, SqlHandle VARBINARY(64), QueryHash BINARY(8), - variable_name NVARCHAR(256), - variable_datatype NVARCHAR(256), - converted_column_name NVARCHAR(256), - compile_time_value NVARCHAR(4000), + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + converted_column_name NVARCHAR(258), + compile_time_value NVARCHAR(258), proc_name NVARCHAR(1000), - column_name NVARCHAR(256), - converted_to NVARCHAR(256) + column_name NVARCHAR(4000), + converted_to NVARCHAR(258), + set_options NVARCHAR(1000) ); CREATE TABLE #variable_info @@ -1206,9 +1223,9 @@ CREATE TABLE #variable_info QueryHash BINARY(8), SqlHandle VARBINARY(64), proc_name NVARCHAR(1000), - variable_name NVARCHAR(256), - variable_datatype NVARCHAR(256), - compile_time_value NVARCHAR(4000) + variable_name NVARCHAR(258), + variable_datatype NVARCHAR(258), + compile_time_value NVARCHAR(258) ); CREATE TABLE #conversion_info @@ -1216,7 +1233,7 @@ CREATE TABLE #conversion_info SPID INT, QueryHash BINARY(8), SqlHandle VARBINARY(64), - proc_name NVARCHAR(256), + proc_name NVARCHAR(258), expression NVARCHAR(4000), at_charindex AS CHARINDEX('@', expression), bracket_charindex AS CHARINDEX(']', expression, CHARINDEX('@', expression)) - CHARINDEX('@', expression), @@ -1286,14 +1303,34 @@ CREATE TABLE #missing_index_pretty database_name NVARCHAR(128), schema_name NVARCHAR(128), table_name NVARCHAR(128), - equality NVARCHAR(4000), - inequality NVARCHAR(4000), - [include] NVARCHAR(4000), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128), + is_spool BIT, details AS N'/* ' + CHAR(10) - + N'The Query Processor estimates that implementing the following index could improve the query cost by ' + + CASE is_spool + WHEN 0 + THEN N'The Query Processor estimates that implementing the ' + ELSE N'We estimate that implementing the ' + END + + N'following index could improve query cost (' + query_cost + N')' + + CHAR(10) + + N'by ' + CONVERT(NVARCHAR(30), impact) - + '%.' + + N'% for ' + executions + N' executions of the query' + + N' over the last ' + + CASE WHEN creation_hours < 24 + THEN creation_hours + N' hours.' + WHEN creation_hours = 24 + THEN ' 1 day.' + WHEN creation_hours > 24 + THEN (CONVERT(NVARCHAR(128), creation_hours / 24)) + N' days.' + ELSE N'' + END + CHAR(10) + N'*/' + CHAR(10) + CHAR(13) @@ -1307,7 +1344,7 @@ CREATE TABLE #missing_index_pretty + N'CREATE NONCLUSTERED INDEX ix_' + ISNULL(REPLACE(REPLACE(REPLACE(equality,'[', ''), ']', ''), ', ', '_'), '') + ISNULL(REPLACE(REPLACE(REPLACE(inequality,'[', ''), ']', ''), ', ', '_'), '') - + CASE WHEN [include] IS NOT NULL THEN + N'Includes' ELSE N'' END + + CASE WHEN [include] IS NOT NULL THEN + N'_Includes' ELSE N'' END + CHAR(10) + N' ON ' + schema_name @@ -1325,8 +1362,8 @@ CREATE TABLE #missing_index_pretty + N')' + CHAR(10) + CASE WHEN include IS NOT NULL - THEN N'INCLUDE (' + include + N')' - ELSE N'' + THEN N'INCLUDE (' + include + N') WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' + ELSE N' WITH (FILLFACTOR=100, ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?);' END + CHAR(10) + N'GO' @@ -1334,6 +1371,49 @@ CREATE TABLE #missing_index_pretty + N'*/' ); + +CREATE TABLE #index_spool_ugly +( + QueryHash BINARY(8), + SqlHandle VARBINARY(64), + impact FLOAT, + database_name NVARCHAR(128), + schema_name NVARCHAR(128), + table_name NVARCHAR(128), + equality NVARCHAR(MAX), + inequality NVARCHAR(MAX), + [include] NVARCHAR(MAX), + executions NVARCHAR(128), + query_cost NVARCHAR(128), + creation_hours NVARCHAR(128) +); + + +CREATE TABLE #ReadableDBs +( +database_id INT +); + + +CREATE TABLE #plan_usage +( + duplicate_plan_hashes BIGINT NULL, + percent_duplicate DECIMAL(9, 2) NULL, + single_use_plan_count BIGINT NULL, + percent_single DECIMAL(9, 2) NULL, + total_plans BIGINT NULL, + spid INT +); + + +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; + + EXEC('INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME OPTION (RECOMPILE);'); + EXEC('INSERT INTO #ReadableDBs VALUES (32767) ;'); -- Exclude internal resource database as well +END + RAISERROR(N'Checking plan cache age', 0, 1) WITH NOWAIT; WITH x AS ( SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THEN 1 ELSE 0 END) AS [plans_24], @@ -1343,15 +1423,93 @@ SELECT SUM(CASE WHEN DATEDIFF(HOUR, deqs.creation_time, SYSDATETIME()) <= 24 THE FROM sys.dm_exec_query_stats AS deqs ) INSERT INTO #plan_creation ( percent_24, percent_4, percent_1, total_plans, SPID ) -SELECT CONVERT(DECIMAL(3,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], - CONVERT(DECIMAL(3,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], +SELECT CONVERT(DECIMAL(5,2), NULLIF(x.plans_24, 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_24], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_4 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_4], + CONVERT(DECIMAL(5,2), NULLIF(x.plans_1 , 0) / (1. * NULLIF(x.total_plans, 0))) * 100 AS [percent_1], x.total_plans, @@SPID AS SPID FROM x -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); +RAISERROR(N'Checking for single use plans and plans with many queries', 0, 1) WITH NOWAIT; +WITH total_plans AS +( + SELECT + COUNT_BIG(deqs.query_plan_hash) AS total_plans + FROM sys.dm_exec_query_stats AS deqs +), + many_plans AS +( + SELECT + SUM(x.duplicate_plan_hashes) AS duplicate_plan_hashes + FROM + ( + SELECT + COUNT_BIG(qs.query_plan_hash) AS duplicate_plan_hashes + FROM sys.dm_exec_query_stats qs + LEFT JOIN sys.dm_exec_procedure_stats ps ON qs.plan_handle = ps.plan_handle + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = N'dbid' + AND pa.value <> 32767 /*Omit Resource database-based queries, we're not going to "fix" them no matter what. Addresses #3314*/ + AND qs.query_plan_hash <> 0x0000000000000000 + GROUP BY + /* qs.query_plan_hash, BGO 20210524 commenting this out to fix #2909 */ + qs.query_hash, + ps.object_id, + pa.value + HAVING COUNT_BIG(qs.query_plan_hash) > 5 + ) AS x +), + single_use_plans AS +( + SELECT + COUNT_BIG(*) AS single_use_plan_count + FROM sys.dm_exec_query_stats AS s + WHERE s.execution_count = 1 +) +INSERT + #plan_usage +( + duplicate_plan_hashes, + percent_duplicate, + single_use_plan_count, + percent_single, + total_plans, + spid +) +SELECT + m.duplicate_plan_hashes, + CONVERT + ( + decimal(5,2), + m.duplicate_plan_hashes + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_duplicate, + s.single_use_plan_count, + CONVERT + ( + decimal(5,2), + s.single_use_plan_count + / (1. * NULLIF(t.total_plans, 0)) + ) * 100. AS percent_single, + t.total_plans, + @@SPID +FROM many_plans AS m +CROSS JOIN single_use_plans AS s +CROSS JOIN total_plans AS t; + + +/* +Erik Darling: + Quoting this out to see if the above query fixes the issue + 2021-05-17, Issue #2909 + +UPDATE #plan_usage + SET percent_duplicate = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END, + percent_single = CASE WHEN percent_duplicate > 100 THEN 100 ELSE percent_duplicate END; +*/ + SET @OnlySqlHandles = LTRIM(RTRIM(@OnlySqlHandles)) ; SET @OnlyQueryHashes = LTRIM(RTRIM(@OnlyQueryHashes)) ; SET @IgnoreQueryHashes = LTRIM(RTRIM(@IgnoreQueryHashes)) ; @@ -1446,14 +1604,35 @@ IF @StoredProcName IS NOT NULL AND @StoredProcName <> N'' BEGIN RAISERROR(N'Setting up filter for stored procedure name', 0, 1) WITH NOWAIT; - INSERT #only_sql_handles + + DECLARE @function_search_sql NVARCHAR(MAX) = N'' + + INSERT #only_sql_handles ( sql_handle ) SELECT ISNULL(deps.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) FROM sys.dm_exec_procedure_stats AS deps WHERE OBJECT_NAME(deps.object_id, deps.database_id) = @StoredProcName - OPTION (RECOMPILE) ; - IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 + UNION ALL + + SELECT ISNULL(dets.sql_handle, CONVERT(VARBINARY(64),'0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')) + FROM sys.dm_exec_trigger_stats AS dets + WHERE OBJECT_NAME(dets.object_id, dets.database_id) = @StoredProcName + OPTION (RECOMPILE); + + IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_search_sql = @function_search_sql + N' + SELECT ISNULL(defs.sql_handle, CONVERT(VARBINARY(64),''0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'')) + FROM sys.dm_exec_function_stats AS defs + WHERE OBJECT_NAME(defs.object_id, defs.database_id) = @i_StoredProcName + OPTION (RECOMPILE); + ' + INSERT #only_sql_handles ( sql_handle ) + EXEC sys.sp_executesql @function_search_sql, N'@i_StoredProcName NVARCHAR(128)', @StoredProcName + END + + IF (SELECT COUNT(*) FROM #only_sql_handles) = 0 BEGIN RAISERROR(N'No information for that stored procedure was found.', 0, 1) WITH NOWAIT; RETURN; @@ -1591,16 +1770,18 @@ SELECT @v = common_version , FROM #checkversion OPTION (RECOMPILE); -IF (@SortOrder IN ('memory grant', 'avg memory grant')) -AND ((@v < 11) -OR (@v = 11 AND @build < 6020) -OR (@v = 12 AND @build < 5000) -OR (@v = 13 AND @build < 1601)) +IF (@SortOrder IN ('memory grant', 'avg memory grant')) AND @VersionShowsMemoryGrants = 0 BEGIN RAISERROR('Your version of SQL does not support sorting by memory grant or average memory grant. Please use another sort order.', 16, 1); RETURN; END; +IF (@SortOrder IN ('spills', 'avg spills') AND @VersionShowsSpills = 0) +BEGIN + RAISERROR('Your version of SQL does not support sorting by spills. Please use another sort order.', 16, 1); + RETURN; +END; + IF ((LEFT(@QueryFilter, 3) = 'fun') AND (@v < 13)) BEGIN RAISERROR('Your version of SQL does not support filtering by functions. Please use another filter.', 16, 1); @@ -1610,14 +1791,15 @@ END; RAISERROR (N'Creating dynamic SQL based on SQL Server version.',0,1) WITH NOWAIT; SET @insert_list += N' -INSERT INTO ##bou_BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO ##BlitzCacheProcs (SPID, QueryType, DatabaseName, AverageCPU, TotalCPU, AverageCPUPerMinute, PercentCPUByType, PercentDurationByType, PercentReadsByType, PercentExecutionsByType, AverageDuration, TotalDuration, AverageReads, TotalReads, ExecutionCount, ExecutionsPerMinute, TotalWrites, AverageWrites, PercentWritesByType, WritesPerMinute, PlanCreationTime, - LastExecutionTime, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, - LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, + LastExecutionTime, LastCompletionTime, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, + LastReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryText, QueryPlan, TotalWorkerTimeForType, TotalElapsedTimeForType, TotalReadsForType, TotalExecutionCountForType, TotalWritesForType, SqlHandle, PlanHandle, QueryHash, QueryPlanHash, - min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime) ' ; + min_worker_time, max_worker_time, is_parallel, min_elapsed_time, max_elapsed_time, age_minutes, age_minutes_lifetime, Pattern) ' ; SET @body += N' FROM (SELECT TOP (@Top) x.*, xpa.*, @@ -1631,19 +1813,34 @@ FROM (SELECT TOP (@Top) x.*, xpa.*, CROSS APPLY (SELECT * FROM sys.dm_exec_plan_attributes(x.plan_handle) AS ixpa WHERE ixpa.attribute = ''dbid'') AS xpa ' + @nl ; +IF @SortOrder = 'duplicate' /* Issue #3345 */ + BEGIN + SET @body += N' INNER JOIN #duplicate_query_filter AS dqf ON x.sql_handle = dqf.sql_handle AND x.plan_handle = dqf.plan_handle AND x.creation_time = dqf.duplicate_creation_time ' + @nl ; + END + +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(x.plan_handle) AS deqps ' + @nl ; + END + SET @body += N' WHERE 1 = 1 ' + @nl ; + 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 ; + END IF @IgnoreSystemDBs = 1 BEGIN RAISERROR(N'Ignoring system databases by default', 0, 1) WITH NOWAIT; - SET @body += N' AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + SET @body += N' AND COALESCE(LOWER(DB_NAME(CAST(xpa.value AS INT))), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(CAST(xpa.value AS INT)), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; END; -IF @DatabaseName IS NOT NULL OR @DatabaseName <> '' +IF @DatabaseName IS NOT NULL OR @DatabaseName <> N'' BEGIN RAISERROR(N'Filtering database name chosen', 0, 1) WITH NOWAIT; - SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(' + SET @body += N' AND CAST(xpa.value AS BIGINT) = DB_ID(N' + QUOTENAME(@DatabaseName, N'''') + N') ' + @nl; END; @@ -1678,7 +1875,6 @@ BEGIN END; /* end filtering for query hashes */ - IF @DurationFilter IS NOT NULL BEGIN RAISERROR(N'Setting duration filter', 0, 1) WITH NOWAIT; @@ -1688,9 +1884,17 @@ IF @DurationFilter IS NOT NULL IF @MinutesBack IS NOT NULL BEGIN RAISERROR(N'Setting minutes back filter', 0, 1) WITH NOWAIT; - SET @body += N' AND x.last_execution_time >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; + SET @body += N' AND DATEADD(SECOND, (x.last_elapsed_time / 1000000.), x.last_execution_time) >= DATEADD(MINUTE, @min_back, GETDATE()) ' + @nl ; END; +IF @SlowlySearchPlansFor IS NOT NULL + BEGIN + RAISERROR(N'Setting string search for @SlowlySearchPlansFor, so remember, this is gonna be slow', 0, 1) WITH NOWAIT; + SET @SlowlySearchPlansFor = REPLACE((REPLACE((REPLACE((REPLACE(@SlowlySearchPlansFor, N'[', N'_')), N']', N'_')), N'^', N'_')), N'''', N''''''); + SET @body_where += N' AND CAST(qp.query_plan AS NVARCHAR(MAX)) LIKE N''%' + @SlowlySearchPlansFor + N'%'' ' + @nl; + END + + /* Apply the sort order here to only grab relevant plans. This should make it faster to process since we'll be pulling back fewer plans for processing. @@ -1704,13 +1908,17 @@ SELECT @body += N' ORDER BY ' + WHEN N'executions' THEN N'execution_count' WHEN N'compiles' THEN N'cached_time' WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ /* And now the averages */ WHEN N'avg cpu' THEN N'total_worker_time / execution_count' WHEN N'avg reads' THEN N'total_logical_reads / execution_count' WHEN N'avg writes' THEN N'total_logical_writes / execution_count' WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN 'CASE WHEN execution_count = 0 THEN 0 WHEN COALESCE(CAST((CASE WHEN DATEDIFF(mi, cached_time, GETDATE()) > 0 AND execution_count > 1 THEN DATEDIFF(mi, cached_time, GETDATE()) ELSE NULL END) as MONEY), CAST((CASE WHEN DATEDIFF(mi, cached_time, last_execution_time) > 0 AND execution_count > 1 @@ -1730,16 +1938,24 @@ SET @body += N') AS qs CROSS JOIN(SELECT SUM(execution_count) AS t_TotalExecs, SUM(CAST(total_elapsed_time AS BIGINT) / 1000.0) AS t_TotalElapsed, SUM(CAST(total_worker_time AS BIGINT) / 1000.0) AS t_TotalWorker, - SUM(CAST(total_logical_reads AS BIGINT)) AS t_TotalReads, - SUM(CAST(total_logical_writes AS BIGINT)) AS t_TotalWrites + SUM(CAST(total_logical_reads AS DECIMAL(30))) AS t_TotalReads, + SUM(CAST(total_logical_writes AS DECIMAL(30))) AS t_TotalWrites FROM sys.#view#) AS t CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) AS pa CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) AS qp ' + @nl ; -SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; +IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @body += N' CROSS APPLY sys.dm_exec_query_plan_stats(qs.plan_handle) AS deqps ' + @nl ; + END +SET @body_where += N' AND pa.attribute = ' + QUOTENAME('dbid', @q ) + @nl ; +IF @NoobSaibot = 1 +BEGIN + SET @body_where += N' AND qp.query_plan.exist(''declare namespace p="http://schemas.microsoft.com/sqlserver/2004/07/showplan";//p:StmtSimple//p:MissingIndex'') = 1' + @nl ; +END SET @plans_triggers_select_list += N' SELECT TOP (@Top) @@ -1748,7 +1964,7 @@ SELECT TOP (@Top) + QUOTENAME(COALESCE(OBJECT_SCHEMA_NAME(qs.object_id, qs.database_id),'''')) + ''.'' + QUOTENAME(COALESCE(OBJECT_NAME(qs.object_id, qs.database_id),'''')) AS QueryType, - COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), ''-- N/A --'') AS DatabaseName, + COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), N''-- N/A --'') AS DatabaseName, (total_worker_time / 1000.0) / execution_count AS AvgCPU , (total_worker_time / 1000.0) AS TotalCPU , CASE WHEN total_worker_time = 0 THEN 0 @@ -1787,8 +2003,10 @@ SELECT TOP (@Top) END AS WritesPerMinute, qs.cached_time AS PlanCreationTime, qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, NULL AS StatementStartOffset, NULL AS StatementEndOffset, + NULL AS PlanGenerationNum, NULL AS MinReturnedRows, NULL AS MaxReturnedRows, NULL AS AvgReturnedRows, @@ -1799,10 +2017,41 @@ SELECT TOP (@Top) NULL AS MinUsedGrantKB, NULL AS MaxUsedGrantKB, NULL AS PercentMemoryGrantUsed, - NULL AS AvgMaxMemoryGrant, - st.text AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, + NULL AS AvgMaxMemoryGrant,'; + + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, '; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @plans_triggers_select_list += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; + + SET @plans_triggers_select_list += + N'st.text AS QueryText ,'; + + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @plans_triggers_select_list += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END; + ELSE + BEGIN + SET @plans_triggers_select_list += N' qp.query_plan AS QueryPlan, ' + @nl ; + END; + + SET @plans_triggers_select_list += + N't.t_TotalWorker, t.t_TotalElapsed, t.t_TotalReads, t.t_TotalExecs, @@ -1817,7 +2066,8 @@ SELECT TOP (@Top) qs.min_elapsed_time / 1000.0, qs.max_elapsed_time / 1000.0, age_minutes, - age_minutes_lifetime '; + age_minutes_lifetime, + @SortOrder '; IF LEFT(@QueryFilter, 3) IN ('all', 'sta') @@ -1828,7 +2078,7 @@ BEGIN SELECT TOP (@Top) @@SPID , ''Statement'' AS QueryType, - COALESCE(DB_NAME(CAST(pa.value AS INT)), ''-- N/A --'') AS DatabaseName, + COALESCE(DB_NAME(CAST(pa.value AS INT)), N''-- N/A --'') AS DatabaseName, (total_worker_time / 1000.0) / execution_count AS AvgCPU , (total_worker_time / 1000.0) AS TotalCPU , CASE WHEN total_worker_time = 0 THEN 0 @@ -1865,8 +2115,10 @@ BEGIN END AS WritesPerMinute, qs.creation_time AS PlanCreationTime, qs.last_execution_time AS LastExecutionTime, + DATEADD(SECOND, (qs.last_elapsed_time / 1000000.), qs.last_execution_time) AS LastCompletionTime, qs.statement_start_offset AS StatementStartOffset, - qs.statement_end_offset AS StatementEndOffset, '; + qs.statement_end_offset AS StatementEndOffset, + qs.plan_generation_num AS PlanGenerationNum, '; IF (@v >= 11) OR (@v >= 10.5 AND @build >= 2500) BEGIN @@ -1889,8 +2141,7 @@ BEGIN NULL AS LastReturnedRows, ' ; END; - IF (@v = 11 AND @build >= 6020) OR (@v = 12 AND @build >= 5000) OR (@v = 13 AND @build >= 1601) - + IF @VersionShowsMemoryGrants = 1 BEGIN RAISERROR(N'Getting memory grant information for newer versions of SQL', 0, 1) WITH NOWAIT; SET @sql += N' @@ -1898,8 +2149,8 @@ BEGIN max_grant_kb AS MaxGrantKB, min_used_grant_kb AS MinUsedGrantKB, max_used_grant_kb AS MaxUsedGrantKB, - CAST(ISNULL(NULLIF(( max_used_grant_kb * 1.00 ), 0) / NULLIF(min_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, - CAST(ISNULL(NULLIF(( max_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; + CAST(ISNULL(NULLIF(( total_used_grant_kb * 1.00 ), 0) / NULLIF(total_grant_kb, 0), 0) * 100. AS MONEY) AS PercentMemoryGrantUsed, + CAST(ISNULL(NULLIF(( total_grant_kb * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgMaxMemoryGrant, '; END; ELSE BEGIN @@ -1913,15 +2164,44 @@ BEGIN NULL AS AvgMaxMemoryGrant, ' ; END; + IF @VersionShowsSpills = 1 + BEGIN + RAISERROR(N'Getting spill information for newer versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills,'; + END; + ELSE + BEGIN + RAISERROR(N'Substituting NULLs for spill columns in older versions of SQL', 0, 1) WITH NOWAIT; + SET @sql += N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ' ; + END; SET @sql += N' SUBSTRING(st.text, ( qs.statement_start_offset / 2 ) + 1, ( ( CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(st.text) ELSE qs.statement_end_offset - END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , - query_plan AS QueryPlan, - t.t_TotalWorker, - t.t_TotalElapsed, + END - qs.statement_start_offset ) / 2 ) + 1) AS QueryText , ' + @nl ; + + + IF @VersionShowsAirQuoteActualPlans = 1 + BEGIN + SET @sql += N' CASE WHEN DATALENGTH(COALESCE(deqps.query_plan,'''')) > DATALENGTH(COALESCE(qp.query_plan,'''')) THEN deqps.query_plan ELSE qp.query_plan END AS QueryPlan, ' + @nl ; + END + ELSE + BEGIN + SET @sql += N' query_plan AS QueryPlan, ' + @nl ; + END + + SET @sql += N' + t.t_TotalWorker, + t.t_TotalElapsed, t.t_TotalReads, t.t_TotalExecs, t.t_TotalWrites, @@ -1935,11 +2215,37 @@ BEGIN qs.min_elapsed_time / 1000.0, qs.max_worker_time / 1000.0, age_minutes, - age_minutes_lifetime '; + age_minutes_lifetime, + @SortOrder '; SET @sql += REPLACE(REPLACE(@body, '#view#', 'dm_exec_query_stats'), 'cached_time', 'creation_time') ; - + + SET @sort_filter += CASE @SortOrder WHEN N'cpu' THEN N'AND total_worker_time > 0' + WHEN N'reads' THEN N'AND total_logical_reads > 0' + WHEN N'writes' THEN N'AND total_logical_writes > 0' + WHEN N'duration' THEN N'AND total_elapsed_time > 0' + WHEN N'executions' THEN N'AND execution_count > 0' + /* WHEN N'compiles' THEN N'AND (age_minutes + age_minutes_lifetime) > 0' BGO 2021-01-24 commenting out for https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2772 */ + WHEN N'memory grant' THEN N'AND max_grant_kb > 0' + WHEN N'unused grant' THEN N'AND max_grant_kb > 0' + WHEN N'spills' THEN N'AND max_spills > 0' + /* And now the averages */ + WHEN N'avg cpu' THEN N'AND (total_worker_time / execution_count) > 0' + WHEN N'avg reads' THEN N'AND (total_logical_reads / execution_count) > 0' + WHEN N'avg writes' THEN N'AND (total_logical_writes / execution_count) > 0' + WHEN N'avg duration' THEN N'AND (total_elapsed_time / execution_count) > 0' + WHEN N'avg memory grant' THEN N'AND CASE WHEN max_grant_kb = 0 THEN 0 ELSE (max_grant_kb / execution_count) END > 0' + WHEN N'avg spills' THEN N'AND CASE WHEN total_spills = 0 THEN 0 ELSE (total_spills / execution_count) END > 0' + WHEN N'avg executions' THEN N'AND CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END > 0' + ELSE N' /* No minimum threshold set */ ' + END; + SET @sql += REPLACE(@body_where, 'cached_time', 'creation_time') ; + + SET @sql += @sort_filter + @nl; SET @sql += @body_order + @nl + @nl + @nl; @@ -1954,7 +2260,7 @@ END; IF (@QueryFilter = 'all' AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ OR (LEFT(@QueryFilter, 3) = 'pro') BEGIN SET @sql += @insert_list; @@ -1964,152 +2270,424 @@ BEGIN SET @sql += @body_where ; IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - SET @sql += @body_order + @nl + @nl + @nl ; + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; END; IF (@v >= 13 AND @QueryFilter = 'all' AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0) - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ + AND (@SortOrder NOT IN ('spills', 'avg spills')) OR (LEFT(@QueryFilter, 3) = 'fun') BEGIN SET @sql += @insert_list; - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') ; + SET @sql += REPLACE(REPLACE(@plans_triggers_select_list, '#query_type#', 'Function') + , N' + min_spills AS MinSpills, + max_spills AS MaxSpills, + total_spills AS TotalSpills, + CAST(ISNULL(NULLIF(( total_spills * 1. ), 0) / NULLIF(execution_count, 0), 0) AS MONEY) AS AvgSpills, ', + N' + NULL AS MinSpills, + NULL AS MaxSpills, + NULL AS TotalSpills, + NULL AS AvgSpills, ') ; SET @sql += REPLACE(@body, '#view#', 'dm_exec_function_stats') ; SET @sql += @body_where ; IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; +END; + +/******************************************************************************* + * + * Because the trigger execution count in SQL Server 2008R2 and earlier is not + * correct, we ignore triggers for these versions of SQL Server. If you'd like + * to include trigger numbers, just know that the ExecutionCount, + * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for + * triggers on these versions of SQL Server. + * + * This is why we can't have nice things. + * + ******************************************************************************/ +IF (@UseTriggersAnyway = 1 OR @v >= 11) + AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 + AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 + AND (@QueryFilter = 'all') + AND (@SortOrder NOT IN ('memory grant', 'avg memory grant', 'unused grant', 'duplicate')) /* Issue #3345 added 'duplicate' */ +BEGIN + RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; + + /* Trigger level information from the plan cache */ + SET @sql += @insert_list ; + + SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; + + SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; + + SET @sql += @body_where ; + + IF @IgnoreSystemDBs = 1 + SET @sql += N' AND COALESCE(LOWER(DB_NAME(database_id)), LOWER(CAST(pa.value AS sysname)), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'', ''dbmaintenance'', ''dbadmin'', ''dbatools'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; + + SET @sql += @sort_filter + @nl; + + SET @sql += @body_order + @nl + @nl + @nl ; +END; + + + +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' + WHEN N'reads' THEN N'total_logical_reads' + WHEN N'writes' THEN N'total_logical_writes' + WHEN N'duration' THEN N'total_elapsed_time' + WHEN N'executions' THEN N'execution_count' + WHEN N'compiles' THEN N'cached_time' + WHEN N'memory grant' THEN N'max_grant_kb' + WHEN N'unused grant' THEN N'max_grant_kb - max_used_grant_kb' + WHEN N'spills' THEN N'max_spills' + WHEN N'duplicate' THEN N'total_worker_time' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'total_worker_time / execution_count' + WHEN N'avg reads' THEN N'total_logical_reads / execution_count' + WHEN N'avg writes' THEN N'total_logical_writes / execution_count' + WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' + WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' + WHEN N'avg spills' THEN N'CASE WHEN total_spills = 0 THEN 0 ELSE total_spills / execution_count END' + WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; + +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); + +SET @sql += N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; +INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) +SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount +FROM (SELECT SqlHandle, + TotalCPU, + TotalReads, + TotalDuration, + TotalWrites, + ExecutionCount, + ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn + FROM ##BlitzCacheProcs + WHERE SPID = @@SPID) AS x +WHERE x.rn = 1 +OPTION (RECOMPILE); + +/* + This block was used to delete duplicate queries, but has been removed. + For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2026 +WITH d AS ( +SELECT SPID, + ROW_NUMBER() OVER (PARTITION BY SqlHandle, QueryHash ORDER BY #sortable# DESC) AS rn +FROM ##BlitzCacheProcs +WHERE SPID = @@SPID +) +DELETE d +WHERE d.rn > 1 +AND SPID = @@SPID +OPTION (RECOMPILE); +*/ +'; + +SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' + WHEN N'reads' THEN N'TotalReads' + WHEN N'writes' THEN N'TotalWrites' + WHEN N'duration' THEN N'TotalDuration' + WHEN N'executions' THEN N'ExecutionCount' + WHEN N'compiles' THEN N'PlanCreationTime' + WHEN N'memory grant' THEN N'MaxGrantKB' + WHEN N'unused grant' THEN N'MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N'MaxSpills' + WHEN N'duplicate' THEN N'TotalCPU' /* Issue #3345 */ + /* And now the averages */ + WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' + WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' + WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' + WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' + WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N'AvgSpills' + WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 + WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 + ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) + END' + END ; + +SELECT @sql = REPLACE(@sql, '#sortable#', @sort); + + +IF @Debug = 1 + BEGIN + PRINT N'Printing dynamic SQL stored in @sql: '; + PRINT SUBSTRING(@sql, 0, 4000); + PRINT SUBSTRING(@sql, 4000, 8000); + PRINT SUBSTRING(@sql, 8000, 12000); + PRINT SUBSTRING(@sql, 12000, 16000); + PRINT SUBSTRING(@sql, 16000, 20000); + PRINT SUBSTRING(@sql, 20000, 24000); + PRINT SUBSTRING(@sql, 24000, 28000); + PRINT SUBSTRING(@sql, 28000, 32000); + PRINT SUBSTRING(@sql, 32000, 36000); + PRINT SUBSTRING(@sql, 36000, 40000); + END; + +RAISERROR(N'Creating temp tables for results and warnings.', 0, 1) WITH NOWAIT; + + +IF OBJECT_ID('tempdb.dbo.##BlitzCacheResults') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheResults ( + SPID INT, + ID INT IDENTITY(1,1), + CheckID INT, + Priority TINYINT, + FindingsGroup VARCHAR(50), + Finding VARCHAR(500), + URL VARCHAR(200), + Details VARCHAR(4000) + ); +END; +ELSE +BEGIN + RAISERROR(N'Cleaning up old warnings for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheResults + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END + - SET @sql += @body_order + @nl + @nl + @nl ; +IF OBJECT_ID('tempdb.dbo.##BlitzCacheProcs') IS NULL +BEGIN + CREATE TABLE ##BlitzCacheProcs ( + SPID INT , + QueryType NVARCHAR(258), + DatabaseName sysname, + AverageCPU DECIMAL(38,4), + AverageCPUPerMinute DECIMAL(38,4), + TotalCPU DECIMAL(38,4), + PercentCPUByType MONEY, + PercentCPU MONEY, + AverageDuration DECIMAL(38,4), + TotalDuration DECIMAL(38,4), + PercentDuration MONEY, + PercentDurationByType MONEY, + AverageReads BIGINT, + TotalReads BIGINT, + PercentReads MONEY, + PercentReadsByType MONEY, + ExecutionCount BIGINT, + PercentExecutions MONEY, + PercentExecutionsByType MONEY, + ExecutionsPerMinute MONEY, + TotalWrites BIGINT, + AverageWrites MONEY, + PercentWrites MONEY, + PercentWritesByType MONEY, + WritesPerMinute MONEY, + PlanCreationTime DATETIME, + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime DATETIME, + LastCompletionTime DATETIME, + PlanHandle VARBINARY(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [PlanHandle], 1) + ');' + ELSE 'N/A' END, + SqlHandle VARBINARY(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'DBCC FREEPROCCACHE (' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ');' + ELSE 'N/A' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlySqlHandles = ''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''; ' + ELSE 'N/A' END, + QueryHash BINARY(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN 'EXEC sp_BlitzCache @OnlyQueryHashes = ''' + CONVERT(VARCHAR(32), [QueryHash], 1) + '''; ' + ELSE 'N/A' END, + QueryPlanHash BINARY(8), + StatementStartOffset INT, + StatementEndOffset INT, + PlanGenerationNum BIGINT, + MinReturnedRows BIGINT, + MaxReturnedRows BIGINT, + AverageReturnedRows MONEY, + TotalReturnedRows BIGINT, + LastReturnedRows BIGINT, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryText NVARCHAR(MAX), + QueryPlan XML, + /* these next four columns are the total for the type of query. + don't actually use them for anything apart from math by type. + */ + TotalWorkerTimeForType BIGINT, + TotalElapsedTimeForType BIGINT, + TotalReadsForType BIGINT, + TotalExecutionCountForType BIGINT, + TotalWritesForType BIGINT, + NumberOfPlans INT, + NumberOfDistinctPlans INT, + SerialDesiredMemory FLOAT, + SerialRequiredMemory FLOAT, + CachedPlanSize FLOAT, + CompileTime FLOAT, + CompileCPU FLOAT , + CompileMemory FLOAT , + MaxCompileMemory FLOAT , + min_worker_time BIGINT, + max_worker_time BIGINT, + is_forced_plan BIT, + is_forced_parameterized BIT, + is_cursor BIT, + is_optimistic_cursor BIT, + is_forward_only_cursor BIT, + is_fast_forward_cursor BIT, + is_cursor_dynamic BIT, + is_parallel BIT, + is_forced_serial BIT, + is_key_lookup_expensive BIT, + key_lookup_cost FLOAT, + is_remote_query_expensive BIT, + remote_query_cost FLOAT, + frequent_execution BIT, + parameter_sniffing BIT, + unparameterized_query BIT, + near_parallel BIT, + plan_warnings BIT, + plan_multiple_plans INT, + long_running BIT, + downlevel_estimator BIT, + implicit_conversions BIT, + busy_loops BIT, + tvf_join BIT, + tvf_estimate BIT, + compile_timeout BIT, + compile_memory_limit_exceeded BIT, + warning_no_join_predicate BIT, + QueryPlanCost FLOAT, + missing_index_count INT, + unmatched_index_count INT, + min_elapsed_time BIGINT, + max_elapsed_time BIGINT, + age_minutes MONEY, + age_minutes_lifetime MONEY, + is_trivial BIT, + trace_flags_session VARCHAR(1000), + is_unused_grant BIT, + function_count INT, + clr_function_count INT, + is_table_variable BIT, + no_stats_warning BIT, + relop_warnings BIT, + is_table_scan BIT, + backwards_scan BIT, + forced_index BIT, + forced_seek BIT, + forced_scan BIT, + columnstore_row_mode BIT, + is_computed_scalar BIT , + is_sort_expensive BIT, + sort_cost FLOAT, + is_computed_filter BIT, + op_name VARCHAR(100) NULL, + index_insert_count INT NULL, + index_update_count INT NULL, + index_delete_count INT NULL, + cx_insert_count INT NULL, + cx_update_count INT NULL, + cx_delete_count INT NULL, + table_insert_count INT NULL, + table_update_count INT NULL, + table_delete_count INT NULL, + index_ops AS (index_insert_count + index_update_count + index_delete_count + + cx_insert_count + cx_update_count + cx_delete_count + + table_insert_count + table_update_count + table_delete_count), + is_row_level BIT, + is_spatial BIT, + index_dml BIT, + table_dml BIT, + long_running_low_cpu BIT, + low_cost_high_cpu BIT, + stale_stats BIT, + is_adaptive BIT, + index_spool_cost FLOAT, + index_spool_rows FLOAT, + table_spool_cost FLOAT, + table_spool_rows FLOAT, + is_spool_expensive BIT, + is_spool_more_rows BIT, + is_table_spool_expensive BIT, + is_table_spool_more_rows BIT, + estimated_rows FLOAT, + is_bad_estimate BIT, + is_paul_white_electric BIT, + is_row_goal BIT, + is_big_spills BIT, + is_mstvf BIT, + is_mm_join BIT, + is_nonsargable BIT, + select_with_writes BIT, + implicit_conversion_info XML, + cached_execution_parameters XML, + missing_indexes XML, + SetOptions VARCHAR(MAX), + Warnings VARCHAR(MAX), + Pattern NVARCHAR(20) + ); END; - - -/******************************************************************************* - * - * Because the trigger execution count in SQL Server 2008R2 and earlier is not - * correct, we ignore triggers for these versions of SQL Server. If you'd like - * to include trigger numbers, just know that the ExecutionCount, - * PercentExecutions, and ExecutionsPerMinute are wildly inaccurate for - * triggers on these versions of SQL Server. - * - * This is why we can't have nice things. - * - ******************************************************************************/ -IF (@UseTriggersAnyway = 1 OR @v >= 11) - AND (SELECT COUNT(*) FROM #only_query_hashes) = 0 - AND (SELECT COUNT(*) FROM #ignore_query_hashes) = 0 - AND (@QueryFilter = 'all') - AND (@SortOrder NOT IN ('memory grant', 'avg memory grant')) +ELSE BEGIN - RAISERROR (N'Adding SQL to collect trigger stats.',0,1) WITH NOWAIT; - - /* Trigger level information from the plan cache */ - SET @sql += @insert_list ; - - SET @sql += REPLACE(@plans_triggers_select_list, '#query_type#', 'Trigger') ; - - SET @sql += REPLACE(@body, '#view#', 'dm_exec_trigger_stats') ; - - SET @sql += @body_where ; - - IF @IgnoreSystemDBs = 1 - SET @sql += N' AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (''master'', ''model'', ''msdb'', ''tempdb'', ''32767'') AND COALESCE(DB_NAME(database_id), CAST(pa.value AS sysname), '''') NOT IN (SELECT name FROM sys.databases WHERE is_distributor = 1)' + @nl ; - - SET @sql += @body_order + @nl + @nl + @nl ; -END; - -DECLARE @sort NVARCHAR(MAX); - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'total_worker_time' - WHEN N'reads' THEN N'total_logical_reads' - WHEN N'writes' THEN N'total_logical_writes' - WHEN N'duration' THEN N'total_elapsed_time' - WHEN N'executions' THEN N'execution_count' - WHEN N'compiles' THEN N'cached_time' - WHEN N'memory grant' THEN N'max_grant_kb' - /* And now the averages */ - WHEN N'avg cpu' THEN N'total_worker_time / execution_count' - WHEN N'avg reads' THEN N'total_logical_reads / execution_count' - WHEN N'avg writes' THEN N'total_logical_writes / execution_count' - WHEN N'avg duration' THEN N'total_elapsed_time / execution_count' - WHEN N'avg memory grant' THEN N'CASE WHEN max_grant_kb = 0 THEN 0 ELSE max_grant_kb / execution_count END' - WHEN N'avg executions' THEN N'CASE WHEN execution_count = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * execution_count / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - -SET @sql += N' -INSERT INTO #p (SqlHandle, TotalCPU, TotalReads, TotalDuration, TotalWrites, ExecutionCount) -SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount -FROM (SELECT SqlHandle, - TotalCPU, - TotalReads, - TotalDuration, - TotalWrites, - ExecutionCount, - ROW_NUMBER() OVER (PARTITION BY SqlHandle ORDER BY #sortable# DESC) AS rn - FROM ##bou_BlitzCacheProcs) AS x -WHERE x.rn = 1 -OPTION (RECOMPILE); -'; - -SELECT @sort = CASE @SortOrder WHEN N'cpu' THEN N'TotalCPU' - WHEN N'reads' THEN N'TotalReads' - WHEN N'writes' THEN N'TotalWrites' - WHEN N'duration' THEN N'TotalDuration' - WHEN N'executions' THEN N'ExecutionCount' - WHEN N'compiles' THEN N'PlanCreationTime' - WHEN N'memory grant' THEN N'MaxGrantKB' - WHEN N'avg cpu' THEN N'TotalCPU / ExecutionCount' - WHEN N'avg reads' THEN N'TotalReads / ExecutionCount' - WHEN N'avg writes' THEN N'TotalWrites / ExecutionCount' - WHEN N'avg duration' THEN N'TotalDuration / ExecutionCount' - WHEN N'avg memory grant' THEN N'AvgMaxMemoryGrant' - WHEN N'avg executions' THEN N'CASE WHEN ExecutionCount = 0 THEN 0 - WHEN COALESCE(age_minutes, age_minutes_lifetime, 0) = 0 THEN 0 - ELSE CAST((1.00 * ExecutionCount / COALESCE(age_minutes, age_minutes_lifetime)) AS money) - END' - END ; - -SELECT @sql = REPLACE(@sql, '#sortable#', @sort); - - -IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@sql, 0, 4000); - PRINT SUBSTRING(@sql, 4000, 8000); - PRINT SUBSTRING(@sql, 8000, 12000); - PRINT SUBSTRING(@sql, 12000, 16000); - PRINT SUBSTRING(@sql, 16000, 20000); - PRINT SUBSTRING(@sql, 20000, 24000); - PRINT SUBSTRING(@sql, 24000, 28000); - PRINT SUBSTRING(@sql, 28000, 32000); - PRINT SUBSTRING(@sql, 32000, 36000); - PRINT SUBSTRING(@sql, 36000, 40000); - END; + RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; + DELETE ##BlitzCacheProcs + WHERE SPID = @@SPID + OPTION (RECOMPILE) ; +END IF @Reanalyze = 0 BEGIN RAISERROR('Collecting execution plan information.', 0, 1) WITH NOWAIT; - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT', @Top, @DurationFilter_i, @MinutesBack; + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @SortOrder NVARCHAR(20)', @Top, @DurationFilter_i, @MinutesBack, @SortOrder; END; +IF @SkipAnalysis = 1 + BEGIN + RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; + GOTO Results ; + END; + -/* Update ##bou_BlitzCacheProcs to get Stored Proc info +/* Update ##BlitzCacheProcs to get Stored Proc info * This should get totals for all statements in a Stored Proc */ RAISERROR(N'Attempting to aggregate stored proc info from separate statements', 0, 1) WITH NOWAIT; @@ -2124,8 +2702,11 @@ RAISERROR(N'Attempting to aggregate stored proc info from separate statements', SUM(b.MinGrantKB) AS MinGrantKB, SUM(b.MaxGrantKB) AS MaxGrantKB, SUM(b.MinUsedGrantKB) AS MinUsedGrantKB, - SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB - FROM ##bou_BlitzCacheProcs b + SUM(b.MaxUsedGrantKB) AS MaxUsedGrantKB, + SUM(b.MinSpills) AS MinSpills, + SUM(b.MaxSpills) AS MaxSpills, + SUM(b.TotalSpills) AS TotalSpills + FROM ##BlitzCacheProcs b WHERE b.SPID = @@SPID AND b.QueryHash IS NOT NULL GROUP BY b.SqlHandle @@ -2140,8 +2721,11 @@ UPDATE b b.MinGrantKB = b2.MinGrantKB, b.MaxGrantKB = b2.MaxGrantKB, b.MinUsedGrantKB = b2.MinUsedGrantKB, - b.MaxUsedGrantKB = b2.MaxUsedGrantKB -FROM ##bou_BlitzCacheProcs b + b.MaxUsedGrantKB = b2.MaxUsedGrantKB, + b.MinSpills = b2.MinSpills, + b.MaxSpills = b2.MaxSpills, + b.TotalSpills = b2.TotalSpills +FROM ##BlitzCacheProcs b JOIN agg b2 ON b2.SqlHandle = b.SqlHandle WHERE b.QueryHash IS NULL @@ -2164,7 +2748,7 @@ SELECT @total_cpu = SUM(TotalCPU), @total_reads = SUM(TotalReads), @total_writes = SUM(TotalWrites), @total_execution_count = SUM(ExecutionCount) -FROM #p +FROM #p OPTION (RECOMPILE) ; DECLARE @cr NVARCHAR(1) = NCHAR(13); @@ -2173,7 +2757,7 @@ DECLARE @tab NVARCHAR(1) = NCHAR(9); /* Update CPU percentage for stored procedures */ RAISERROR(N'Update CPU percentage for stored procedures', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET PercentCPU = y.PercentCPU, PercentDuration = y.PercentDuration, PercentReads = y.PercentReads, @@ -2183,7 +2767,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 @@ -2209,7 +2796,7 @@ FROM ( ExecutionCount, PlanCreationTime, LastExecutionTime - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE PlanHandle IS NOT NULL AND SPID = @@SPID GROUP BY PlanHandle, @@ -2222,14 +2809,14 @@ FROM ( LastExecutionTime ) AS x ) AS y -WHERE ##bou_BlitzCacheProcs.PlanHandle = y.PlanHandle - AND ##bou_BlitzCacheProcs.PlanHandle IS NOT NULL - AND ##bou_BlitzCacheProcs.SPID = @@SPID +WHERE ##BlitzCacheProcs.PlanHandle = y.PlanHandle + AND ##BlitzCacheProcs.PlanHandle IS NOT NULL + AND ##BlitzCacheProcs.SPID = @@SPID OPTION (RECOMPILE) ; RAISERROR(N'Gather percentage information from grouped results', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET PercentCPU = y.PercentCPU, PercentDuration = y.PercentDuration, PercentReads = y.PercentReads, @@ -2239,7 +2826,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, @@ -2269,7 +2859,7 @@ FROM ( ExecutionCount, PlanCreationTime, LastExecutionTime - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE SPID = @@SPID GROUP BY DatabaseName, SqlHandle, @@ -2283,10 +2873,10 @@ FROM ( LastExecutionTime ) AS x ) AS y -WHERE ##bou_BlitzCacheProcs.SqlHandle = y.SqlHandle - AND ##bou_BlitzCacheProcs.QueryHash = y.QueryHash - AND ##bou_BlitzCacheProcs.DatabaseName = y.DatabaseName - AND ##bou_BlitzCacheProcs.PlanHandle IS NULL +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle + AND ##BlitzCacheProcs.QueryHash = y.QueryHash + AND ##BlitzCacheProcs.DatabaseName = y.DatabaseName + AND ##BlitzCacheProcs.PlanHandle IS NULL OPTION (RECOMPILE) ; @@ -2297,9 +2887,10 @@ WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS SELECT QueryHash , SqlHandle , PlanHandle, - q.n.query('.') AS statement + q.n.query('.') AS statement, + 0 AS is_cursor INTO #statements -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p CROSS APPLY p.QueryPlan.nodes('//p:StmtSimple') AS q(n) WHERE p.SPID = @@SPID OPTION (RECOMPILE) ; @@ -2309,8 +2900,9 @@ INSERT #statements SELECT QueryHash , SqlHandle , PlanHandle, - q.n.query('.') AS statement -FROM ##bou_BlitzCacheProcs p + q.n.query('.') AS statement, + 1 AS is_cursor +FROM ##BlitzCacheProcs p CROSS APPLY p.QueryPlan.nodes('//p:StmtCursor') AS q(n) WHERE p.SPID = @@SPID OPTION (RECOMPILE) ; @@ -2333,32 +2925,60 @@ FROM #query_plan p CROSS APPLY p.query_plan.nodes('//p:RelOp') AS q(n) OPTION (RECOMPILE) ; - - -- high level plan stuff RAISERROR(N'Gathering high level plan information', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET NumberOfDistinctPlans = distinct_plan_count, NumberOfPlans = number_of_plans , - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END -FROM ( - SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash - FROM ##bou_BlitzCacheProcs - WHERE SPID = @@SPID - GROUP BY QueryHash + plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN number_of_plans END +FROM + ( + SELECT + DatabaseName = + DB_NAME(CONVERT(int, pa.value)), + QueryHash = + qs.query_hash, + number_of_plans = + COUNT_BIG(qs.query_plan_hash), + distinct_plan_count = + COUNT_BIG(DISTINCT qs.query_plan_hash) + FROM sys.dm_exec_query_stats AS qs + CROSS APPLY sys.dm_exec_plan_attributes(qs.plan_handle) pa + WHERE pa.attribute = 'dbid' + GROUP BY + DB_NAME(CONVERT(int, pa.value)), + qs.query_hash ) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash +WHERE ##BlitzCacheProcs.QueryHash = x.QueryHash +AND ##BlitzCacheProcs.DatabaseName = x.DatabaseName OPTION (RECOMPILE) ; +-- query level checks +RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE ##BlitzCacheProcs +SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , + unmatched_index_count = CASE WHEN is_trivial <> 1 THEN query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') END , + SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , + SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), + CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , + CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , + CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , + CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float'), + MaxCompileMemory = query_plan.value('sum(//p:QueryPlan/p:OptimizerHardwareDependentProperties/@MaxCompileMemory)', 'float') +FROM #query_plan qp +WHERE qp.QueryHash = ##BlitzCacheProcs.QueryHash +AND qp.SqlHandle = ##BlitzCacheProcs.SqlHandle +AND SPID = @@SPID +OPTION (RECOMPILE); + -- statement level checks RAISERROR(N'Performing compile timeout checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b SET compile_timeout = 1 FROM #statements s -JOIN ##bou_BlitzCacheProcs b +JOIN ##BlitzCacheProcs b ON s.QueryHash = b.QueryHash AND SPID = @@SPID WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="TimeOut"]') = 1 @@ -2369,12 +2989,14 @@ WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS UPDATE b SET compile_memory_limit_exceeded = 1 FROM #statements s -JOIN ##bou_BlitzCacheProcs b +JOIN ##BlitzCacheProcs b ON s.QueryHash = b.QueryHash AND SPID = @@SPID WHERE statement.exist('/p:StmtSimple/@StatementOptmEarlyAbortReason[.="MemoryLimitExceeded"]') = 1 OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Performing unparameterized query checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), unparameterized_query AS ( @@ -2388,14 +3010,17 @@ unparameterized_query AS ( ) UPDATE b SET b.unparameterized_query = u.unparameterized_query -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b JOIN unparameterized_query u ON u.QueryHash = b.QueryHash AND SPID = @@SPID WHERE u.unparameterized_query = 1 OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Performing index DML checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), index_dml AS ( @@ -2407,13 +3032,17 @@ index_dml AS ( ) UPDATE b SET b.index_dml = i.index_dml - FROM ##bou_BlitzCacheProcs AS b + FROM ##BlitzCacheProcs AS b JOIN index_dml i ON i.QueryHash = b.QueryHash WHERE i.index_dml = 1 AND b.SPID = @@SPID OPTION (RECOMPILE); +END; + +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Performing table DML checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), table_dml AS ( @@ -2425,14 +3054,17 @@ table_dml AS ( ) UPDATE b SET b.table_dml = t.table_dml - FROM ##bou_BlitzCacheProcs AS b + FROM ##BlitzCacheProcs AS b JOIN table_dml t ON t.QueryHash = b.QueryHash WHERE t.table_dml = 1 AND b.SPID = @@SPID OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Gathering row estimates', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT INTO #est_rows @@ -2445,21 +3077,40 @@ WHERE c.n.exist('/p:StmtSimple[@StatementEstRows > 0]') = 1; UPDATE b SET b.estimated_rows = er.estimated_rows - FROM ##bou_BlitzCacheProcs AS b + FROM ##BlitzCacheProcs AS b JOIN #est_rows er ON er.QueryHash = b.QueryHash WHERE b.SPID = @@SPID AND b.QueryType = 'Statement' OPTION (RECOMPILE); +END; + +RAISERROR(N'Gathering trivial plans', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +UPDATE b +SET b.is_trivial = 1 +FROM ##BlitzCacheProcs AS b +JOIN ( +SELECT s.SqlHandle +FROM #statements AS s +JOIN ( SELECT r.SqlHandle + FROM #relop AS r + WHERE r.relop.exist('//p:RelOp[contains(@LogicalOp, "Scan")]') = 1 ) AS r + ON r.SqlHandle = s.SqlHandle +WHERE s.statement.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 +) AS s +ON b.SqlHandle = s.SqlHandle +OPTION (RECOMPILE); --Gather costs RAISERROR(N'Gathering statement costs', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -INSERT INTO #plan_cost +INSERT INTO #plan_cost ( QueryPlanCost, SqlHandle, PlanHandle, QueryHash, QueryPlanHash ) SELECT DISTINCT statement.value('sum(/p:StmtSimple/@StatementSubTreeCost)', 'float') QueryPlanCost, s.SqlHandle, + s.PlanHandle, CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryHash, CONVERT(BINARY(8), RIGHT('0000000000000000' + SUBSTRING(q.n.value('@QueryPlanHash', 'VARCHAR(18)'), 3, 18), 16), 2) AS QueryPlanHash FROM #statements s @@ -2469,22 +3120,22 @@ OPTION (RECOMPILE); RAISERROR(N'Updating statement costs', 0, 1) WITH NOWAIT; WITH pc AS ( - SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash + SELECT SUM(DISTINCT pc.QueryPlanCost) AS QueryPlanCostSum, pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle FROM #plan_cost AS pc - GROUP BY pc.QueryHash, pc.QueryPlanHash + GROUP BY pc.QueryHash, pc.QueryPlanHash, pc.SqlHandle, pc.PlanHandle ) UPDATE b SET b.QueryPlanCost = ISNULL(pc.QueryPlanCostSum, 0) FROM pc - JOIN ##bou_BlitzCacheProcs b - ON b.QueryPlanHash = pc.QueryPlanHash - OR b.QueryHash = pc.QueryHash + JOIN ##BlitzCacheProcs b + ON b.SqlHandle = pc.SqlHandle + AND b.QueryHash = pc.QueryHash WHERE b.QueryType NOT LIKE '%Procedure%' OPTION (RECOMPILE); IF EXISTS ( SELECT 1 -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b WHERE b.QueryType LIKE 'Procedure%' ) @@ -2510,12 +3161,13 @@ RAISERROR(N'Gathering stored procedure costs', 0, 1) WITH NOWAIT; ) INSERT INTO #proc_costs SELECT qcu.PlanTotalQuery, PlanHandle, SqlHandle -FROM QueryCostUpdate AS qcu; +FROM QueryCostUpdate AS qcu +OPTION (RECOMPILE); UPDATE b SET b.QueryPlanCost = ca.PlanTotalQuery -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b CROSS APPLY ( SELECT TOP 1 PlanTotalQuery FROM #proc_costs qcu @@ -2530,37 +3182,39 @@ END; UPDATE b SET b.QueryPlanCost = 0.0 -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b WHERE b.QueryPlanCost IS NULL AND b.SPID = @@SPID OPTION (RECOMPILE); RAISERROR(N'Checking for plan warnings', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET plan_warnings = 1 FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle AND SPID = @@SPID AND query_plan.exist('/p:QueryPlan/p:Warnings') = 1 OPTION (RECOMPILE); RAISERROR(N'Checking for implicit conversion', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET implicit_conversions = 1 FROM #query_plan qp -WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle +WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle AND SPID = @@SPID AND query_plan.exist('/p:QueryPlan/p:Warnings/p:PlanAffectingConvert/@Expression[contains(., "CONVERT_IMPLICIT")]') = 1 OPTION (RECOMPILE); -- operator level checks +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Performing busy loops checks', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE p SET busy_loops = CASE WHEN (x.estimated_executions / 100.0) > x.estimated_rows THEN 1 END -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p JOIN ( SELECT qs.SqlHandle, relop.value('sum(/p:RelOp/@EstimateRows)', 'float') AS estimated_rows , @@ -2569,12 +3223,14 @@ FROM ##bou_BlitzCacheProcs p ) AS x ON p.SqlHandle = x.SqlHandle WHERE SPID = @@SPID OPTION (RECOMPILE); +END; + RAISERROR(N'Performing TVF join check', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE p SET p.tvf_join = CASE WHEN x.tvf_join = 1 THEN 1 END -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p JOIN ( SELECT r.SqlHandle, 1 AS tvf_join @@ -2585,6 +3241,8 @@ FROM ##bou_BlitzCacheProcs p WHERE SPID = @@SPID OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for operator warnings', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) , x AS ( @@ -2599,10 +3257,11 @@ UPDATE p SET p.warning_no_join_predicate = x.warning_no_join_predicate, p.no_stats_warning = x.no_stats_warning, p.relop_warnings = x.relop_warnings -FROM ##bou_BlitzCacheProcs AS p +FROM ##BlitzCacheProcs AS p JOIN x ON x.SqlHandle = p.SqlHandle AND SPID = @@SPID OPTION (RECOMPILE); +END; RAISERROR(N'Checking for table variables', 0, 1) WITH NOWAIT; @@ -2614,12 +3273,15 @@ FROM #relop r CROSS APPLY r.relop.nodes('//p:Object') AS c(n) ) UPDATE p -SET is_table_variable = CASE WHEN x.first_char = '@' THEN 1 END -FROM ##bou_BlitzCacheProcs AS p +SET is_table_variable = 1 +FROM ##BlitzCacheProcs AS p JOIN x ON x.SqlHandle = p.SqlHandle AND SPID = @@SPID +WHERE x.first_char = '@' OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for functions', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) , x AS ( @@ -2632,72 +3294,136 @@ CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue UPDATE p SET p.function_count = x.function_count, p.clr_function_count = x.clr_function_count -FROM ##bou_BlitzCacheProcs AS p +FROM ##BlitzCacheProcs AS p JOIN x ON x.SqlHandle = p.SqlHandle AND SPID = @@SPID OPTION (RECOMPILE); +END; RAISERROR(N'Checking for expensive key lookups', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET key_lookup_cost = x.key_lookup_cost FROM ( SELECT qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS key_lookup_cost + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS key_lookup_cost FROM #relop qs WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Lookup[.="1"])]') = 1 +GROUP BY qs.SqlHandle ) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); RAISERROR(N'Checking for expensive remote queries', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET remote_query_cost = x.remote_query_cost FROM ( SELECT qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float') AS remote_query_cost + MAX(relop.value('sum(/p:RelOp/@EstimatedTotalSubtreeCost)', 'float')) AS remote_query_cost FROM #relop qs WHERE [relop].exist('/p:RelOp[(@PhysicalOp[contains(., "Remote")])]') = 1 +GROUP BY qs.SqlHandle ) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); RAISERROR(N'Checking for expensive sorts', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET sort_cost = (x.sort_io + x.sort_cpu) +UPDATE ##BlitzCacheProcs +SET sort_cost = y.max_sort_cost FROM ( -SELECT - qs.SqlHandle, - relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, - relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu -FROM #relop qs -WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 -) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle + SELECT x.SqlHandle, MAX((x.sort_io + x.sort_cpu)) AS max_sort_cost + FROM ( + SELECT + qs.SqlHandle, + relop.value('sum(/p:RelOp/@EstimateIO)', 'float') AS sort_io, + relop.value('sum(/p:RelOp/@EstimateCPU)', 'float') AS sort_cpu + FROM #relop qs + WHERE [relop].exist('/p:RelOp[(@PhysicalOp[.="Sort"])]') = 1 + ) AS x + GROUP BY x.SqlHandle + ) AS y +WHERE ##BlitzCacheProcs.SqlHandle = y.SqlHandle AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); + +IF NOT EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'No cursor plans found, skipping', 0, 1) WITH NOWAIT; + +END + +IF EXISTS(SELECT 1/0 FROM #statements AS s WHERE s.is_cursor = 1) +BEGIN + +RAISERROR(N'Cursor plans found, investigating', 0, 1) WITH NOWAIT; -RAISERROR(N'Checking for icky cursors', 0, 1) WITH NOWAIT; +RAISERROR(N'Checking for Optimistic cursors', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b -SET b.is_optimistic_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 THEN 1 END, - b.is_forward_only_cursor = CASE WHEN n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 THEN 1 ELSE 0 END -FROM ##bou_BlitzCacheProcs b +SET b.is_optimistic_cursor = 1 +FROM ##BlitzCacheProcs b JOIN #statements AS qs ON b.SqlHandle = qs.SqlHandle CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) WHERE SPID = @@SPID -OPTION (RECOMPILE) ; +AND n1.fn.exist('//p:CursorPlan/@CursorConcurrency[.="Optimistic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking if cursor is Forward Only', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_forward_only_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@ForwardOnly[.="true"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + +RAISERROR(N'Checking if cursor is Fast Forward', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_fast_forward_cursor = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="FastForward"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); + + +RAISERROR(N'Checking for Dynamic cursors', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_cursor_dynamic = 1 +FROM ##BlitzCacheProcs b +JOIN #statements AS qs +ON b.SqlHandle = qs.SqlHandle +CROSS APPLY qs.statement.nodes('/p:StmtCursor') AS n1(fn) +WHERE SPID = @@SPID +AND n1.fn.exist('//p:CursorPlan/@CursorActualType[.="Dynamic"]') = 1 +AND qs.is_cursor = 1 +OPTION (RECOMPILE); +END +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for bad scans and plan forcing', 0, 1) WITH NOWAIT; ;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE b @@ -2707,7 +3433,7 @@ b.backwards_scan = x.backwards_scan, b.forced_index = x.forced_index, b.forced_seek = x.forced_seek, b.forced_scan = x.forced_scan -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b JOIN ( SELECT qs.SqlHandle, @@ -2730,12 +3456,15 @@ FROM #relop qs CROSS APPLY qs.relop.nodes('//p:TableScan') AS q(n) ) AS x ON b.SqlHandle = x.SqlHandle WHERE SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); +END; +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for computed columns that reference scalar UDFs', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET is_computed_scalar = x.computed_column_function FROM ( SELECT qs.SqlHandle, @@ -2744,14 +3473,15 @@ FROM #relop qs CROSS APPLY relop.nodes('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ScalarOperator') n(fn) WHERE n.fn.exist('/p:RelOp/p:ComputeScalar/p:DefinedValues/p:DefinedValue/p:ColumnReference[(@ComputedColumn[.="1"])]') = 1 ) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID OPTION (RECOMPILE); +END; RAISERROR(N'Checking for filters that reference scalar UDFs', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET is_computed_filter = x.filter_function FROM ( SELECT @@ -2760,10 +3490,12 @@ c.n.value('count(distinct-values(//p:UserDefinedFunction[not(@IsClrFunction)]))' FROM #relop AS r CROSS APPLY r.relop.nodes('/p:RelOp/p:Filter/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator/p:UserDefinedFunction') c(n) ) x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID OPTION (RECOMPILE); +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking modification queries that hit lots of indexes', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), IndexOps AS @@ -2812,14 +3544,18 @@ SET b.index_insert_count = iops.index_insert_count, b.table_insert_count = iops.table_insert_count, b.table_update_count = iops.table_update_count, b.table_delete_count = iops.table_delete_count -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b JOIN iops ON iops.QueryHash = b.QueryHash WHERE SPID = @@SPID -OPTION(RECOMPILE); +OPTION (RECOMPILE); +END; + +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Checking for Spatial index use', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET is_spatial = x.is_spatial FROM ( SELECT qs.SqlHandle, @@ -2828,9 +3564,11 @@ FROM #relop qs CROSS APPLY relop.nodes('/p:RelOp//p:Object') n(fn) WHERE n.fn.exist('(@IndexKind[.="Spatial"])') = 1 ) AS x -WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle +WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID OPTION (RECOMPILE); +END; + RAISERROR('Checking for wonky Index Spools', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( @@ -2844,7 +3582,7 @@ AS ( SELECT DISTINCT r.QueryHash, c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, - c.n.value('@EstimateRewinds', 'FLOAT') AS estimated_rewinds + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds FROM #relop AS r JOIN selects AS s ON s.QueryHash = r.QueryHash @@ -2853,12 +3591,65 @@ WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager S ) UPDATE b SET b.index_spool_rows = sp.estimated_rows, - b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE sp.estimated_rewinds WHEN 0 THEN 1 ELSE sp.estimated_rewinds END) -FROM ##bou_BlitzCacheProcs b + b.index_spool_cost = ((sp.estimated_io * sp.estimated_cpu) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b JOIN spools sp ON sp.QueryHash = b.QueryHash -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); + +RAISERROR('Checking for wonky Table Spools', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT s.QueryHash + FROM #statements AS s + WHERE s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 ) +, spools +AS ( SELECT DISTINCT r.QueryHash, + c.n.value('@EstimateRows', 'FLOAT') AS estimated_rows, + c.n.value('@EstimateIO', 'FLOAT') AS estimated_io, + c.n.value('@EstimateCPU', 'FLOAT') AS estimated_cpu, + c.n.value('@EstimateRebinds', 'FLOAT') AS estimated_rebinds +FROM #relop AS r +JOIN selects AS s +ON s.QueryHash = r.QueryHash +CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) +WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Table Spool" and @LogicalOp="Lazy Spool"]') = 1 +) +UPDATE b + SET b.table_spool_rows = (sp.estimated_rows * sp.estimated_rebinds), + b.table_spool_cost = ((sp.estimated_io * sp.estimated_cpu * sp.estimated_rows) * CASE WHEN sp.estimated_rebinds < 1 THEN 1 ELSE sp.estimated_rebinds END) +FROM ##BlitzCacheProcs b +JOIN spools sp +ON sp.QueryHash = b.QueryHash +OPTION (RECOMPILE); + +RAISERROR('Checking for selects that cause non-spill and index spool writes', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( + 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, selects +AS ( SELECT CONVERT(BINARY(8), + RIGHT('0000000000000000' + + SUBSTRING(s.statement.value('(/p:StmtSimple/@QueryHash)[1]', 'VARCHAR(18)'), + 3, 18), 16), 2) AS QueryHash + FROM #statements AS s + JOIN ##BlitzCacheProcs b + ON s.QueryHash = b.QueryHash + WHERE b.index_spool_rows IS NULL + AND b.index_spool_cost IS NULL + AND b.table_spool_cost IS NULL + AND b.table_spool_rows IS NULL + AND b.is_big_spills IS NULL + AND b.AverageWrites > 1024. + AND s.statement.exist('/p:StmtSimple/@StatementType[.="SELECT"]') = 1 +) +UPDATE b + SET b.select_with_writes = 1 +FROM ##BlitzCacheProcs b +JOIN selects AS s +ON s.QueryHash = b.QueryHash +AND b.AverageWrites > 1024.; /* 2012+ only */ IF @v >= 11 @@ -2866,19 +3657,20 @@ BEGIN RAISERROR(N'Checking for forced serialization', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET is_forced_serial = 1 FROM #query_plan qp - WHERE qp.SqlHandle = ##bou_BlitzCacheProcs.SqlHandle + WHERE qp.SqlHandle = ##BlitzCacheProcs.SqlHandle AND SPID = @@SPID AND query_plan.exist('/p:QueryPlan/@NonParallelPlanReason') = 1 - AND (##bou_BlitzCacheProcs.is_parallel = 0 OR ##bou_BlitzCacheProcs.is_parallel IS NULL) + AND (##BlitzCacheProcs.is_parallel = 0 OR ##BlitzCacheProcs.is_parallel IS NULL) OPTION (RECOMPILE); - + IF @ExpertMode > 0 + BEGIN RAISERROR(N'Checking for ColumnStore queries operating in Row Mode instead of Batch Mode', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET columnstore_row_mode = x.is_row_mode FROM ( SELECT @@ -2887,9 +3679,10 @@ BEGIN FROM #relop qs WHERE [relop].exist('/p:RelOp/p:IndexScan[(@Storage[.="ColumnStore"])]') = 1 ) AS x - WHERE ##bou_BlitzCacheProcs.SqlHandle = x.SqlHandle + WHERE ##BlitzCacheProcs.SqlHandle = x.SqlHandle AND SPID = @@SPID - OPTION (RECOMPILE) ; + OPTION (RECOMPILE); + END; END; @@ -2901,44 +3694,48 @@ BEGIN WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE p SET downlevel_estimator = CASE WHEN statement.value('min(//p:StmtSimple/@CardinalityEstimationModelVersion)', 'int') < (@v * 10) THEN 1 END - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p JOIN #statements s ON p.QueryHash = s.QueryHash WHERE SPID = @@SPID - OPTION (RECOMPILE) ; + OPTION (RECOMPILE); END ; /* 2016+ only */ -IF @v >= 13 +IF @v >= 13 AND @ExpertMode > 0 BEGIN RAISERROR('Checking for row level security in 2016 only', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) UPDATE p SET p.is_row_level = 1 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p JOIN #statements s ON p.QueryHash = s.QueryHash WHERE SPID = @@SPID AND statement.exist('/p:StmtSimple/@SecurityPolicyApplied[.="true"]') = 1 - OPTION (RECOMPILE) ; + OPTION (RECOMPILE); END ; /* 2017+ only */ -IF @v >= 14 +IF @v >= 14 OR (@v = 13 AND @build >= 5026) BEGIN +IF @ExpertMode > 0 +BEGIN RAISERROR('Gathering stats information', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) INSERT INTO #stats_agg SELECT qp.SqlHandle, x.c.value('@LastUpdate', 'DATETIME2(7)') AS LastUpdate, - x.c.value('@ModificationCount', 'INT') AS ModificationCount, + x.c.value('@ModificationCount', 'BIGINT') AS ModificationCount, x.c.value('@SamplingPercent', 'FLOAT') AS SamplingPercent, - x.c.value('@Statistics', 'NVARCHAR(256)') AS [Statistics], - x.c.value('@Table', 'NVARCHAR(256)') AS [Table], - x.c.value('@Schema', 'NVARCHAR(256)') AS [Schema], - x.c.value('@Database', 'NVARCHAR(256)') AS [Database] + x.c.value('@Statistics', 'NVARCHAR(258)') AS [Statistics], + x.c.value('@Table', 'NVARCHAR(258)') AS [Table], + x.c.value('@Schema', 'NVARCHAR(258)') AS [Schema], + x.c.value('@Database', 'NVARCHAR(258)') AS [Database] FROM #query_plan AS qp -CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c); +CROSS APPLY qp.query_plan.nodes('//p:OptimizerStatsUsage/p:StatisticsInfo') x (c) +OPTION (RECOMPILE); + RAISERROR('Checking for stale stats', 0, 1) WITH NOWAIT; WITH stale_stats AS ( @@ -2950,12 +3747,16 @@ WITH stale_stats AS ( ) UPDATE b SET stale_stats = 1 -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b JOIN stale_stats os ON b.SqlHandle = os.SqlHandle AND b.SPID = @@SPID OPTION (RECOMPILE); +END; +IF @v >= 14 AND @ExpertMode > 0 +BEGIN +RAISERROR('Checking for adaptive joins', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), aj AS ( SELECT @@ -2966,50 +3767,39 @@ aj AS ( ) UPDATE b SET b.is_adaptive = 1 -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b JOIN aj ON b.SqlHandle = aj.SqlHandle AND b.SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); +END; -END; +IF ((@v >= 14 + OR (@v = 13 AND @build >= 5026) + OR (@v = 12 AND @build >= 6024)) + AND @ExpertMode > 0) --- query level checks -RAISERROR(N'Performing query level checks', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET missing_index_count = query_plan.value('count(//p:QueryPlan/p:MissingIndexes/p:MissingIndexGroup)', 'int') , - unmatched_index_count = query_plan.value('count(//p:QueryPlan/p:UnmatchedIndexes/p:Parameterization/p:Object)', 'int') , - SerialDesiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialDesiredMemory)', 'float') , - SerialRequiredMemory = query_plan.value('sum(//p:QueryPlan/p:MemoryGrantInfo/@SerialRequiredMemory)', 'float'), - CachedPlanSize = query_plan.value('sum(//p:QueryPlan/@CachedPlanSize)', 'float') , - CompileTime = query_plan.value('sum(//p:QueryPlan/@CompileTime)', 'float') , - CompileCPU = query_plan.value('sum(//p:QueryPlan/@CompileCPU)', 'float') , - CompileMemory = query_plan.value('sum(//p:QueryPlan/@CompileMemory)', 'float') -FROM #query_plan qp -WHERE qp.QueryHash = ##bou_BlitzCacheProcs.QueryHash -AND SPID = @@SPID +BEGIN; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), +row_goals AS( +SELECT qs.QueryHash +FROM #relop qs +WHERE relop.value('sum(/p:RelOp/@EstimateRowsWithoutRowGoal)', 'float') > 0 +) +UPDATE b +SET b.is_row_goal = 1 +FROM ##BlitzCacheProcs b +JOIN row_goals +ON b.QueryHash = row_goals.QueryHash +AND b.SPID = @@SPID OPTION (RECOMPILE); +END ; + +END; /* END Testing using XML nodes to speed up processing */ -RAISERROR(N'Gathering additional plan level information', 0, 1) WITH NOWAIT; -WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs -SET NumberOfDistinctPlans = distinct_plan_count, - NumberOfPlans = number_of_plans, - plan_multiple_plans = CASE WHEN distinct_plan_count < number_of_plans THEN 1 END , - is_trivial = CASE WHEN QueryPlan.exist('//p:StmtSimple[@StatementOptmLevel[.="TRIVIAL"]]/p:QueryPlan/p:ParameterList') = 1 THEN 1 END -FROM ( -SELECT COUNT(DISTINCT QueryHash) AS distinct_plan_count, - COUNT(QueryHash) AS number_of_plans, - QueryHash -FROM ##bou_BlitzCacheProcs -WHERE SPID = @@SPID -GROUP BY QueryHash -) AS x -WHERE ##bou_BlitzCacheProcs.QueryHash = x.QueryHash -OPTION (RECOMPILE) ; + /* Update to grab stored procedure name for individual statements */ RAISERROR(N'Attempting to get stored procedure name for individual statements', 0, 1) WITH NOWAIT; @@ -3018,15 +3808,65 @@ SET QueryType = QueryType + ' (parent ' + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + '.' + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle WHERE QueryType = 'Statement' AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); + +/* Update to grab stored procedure name for individual statements when PSPO is detected */ +UPDATE p +SET QueryType = QueryType + ' (parent ' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + '.' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + ')' +FROM ##BlitzCacheProcs p + OUTER APPLY ( + SELECT REPLACE(REPLACE(REPLACE(REPLACE(p.QueryText, ' (', '('), '( ', '('), ' =', '='), '= ', '=') AS NormalizedQueryText + ) a + OUTER APPLY ( + SELECT CHARINDEX('option(PLAN PER VALUE(ObjectID=', a.NormalizedQueryText) AS OptionStart + ) b + OUTER APPLY ( + SELECT SUBSTRING(a.NormalizedQueryText, b.OptionStart + 31, LEN(a.NormalizedQueryText) - b.OptionStart - 30) AS OptionSubstring + WHERE b.OptionStart > 0 + ) c + OUTER APPLY ( + SELECT PATINDEX('%[^0-9]%', c.OptionSubstring) AS ObjectLength + ) d + OUTER APPLY ( + SELECT TRY_CAST(SUBSTRING(OptionSubstring, 1, d.ObjectLength - 1) AS INT) AS ObjectId + ) e + JOIN sys.dm_exec_procedure_stats s ON DB_ID(p.DatabaseName) = s.database_id AND e.ObjectId = s.object_id +WHERE p.QueryType = 'Statement' +AND p.SPID = @@SPID +AND s.object_id IS NOT NULL +OPTION (RECOMPILE); -/* Trace Flag Checks 2014 SP2 and 2016 SP1 only)*/ +RAISERROR(N'Attempting to get function name for individual statements', 0, 1) WITH NOWAIT; +DECLARE @function_update_sql NVARCHAR(MAX) = N'' +IF EXISTS (SELECT 1/0 FROM sys.all_objects AS o WHERE o.name = 'dm_exec_function_stats') + BEGIN + SET @function_update_sql = @function_update_sql + N' + UPDATE p + SET QueryType = QueryType + '' (parent '' + + + QUOTENAME(OBJECT_SCHEMA_NAME(s.object_id, s.database_id)) + + ''.'' + + QUOTENAME(OBJECT_NAME(s.object_id, s.database_id)) + '')'' + FROM ##BlitzCacheProcs p + JOIN sys.dm_exec_function_stats s ON p.SqlHandle = s.sql_handle + WHERE QueryType = ''Statement'' + AND SPID = @@SPID + OPTION (RECOMPILE); + ' + EXEC sys.sp_executesql @function_update_sql + END + + +/* Trace Flag Checks 2012 SP3, 2014 SP2 and 2016 SP1 only)*/ IF @v >= 11 BEGIN + RAISERROR(N'Trace flag checks', 0, 1) WITH NOWAIT; ;WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) , tf_pretty AS ( @@ -3060,13 +3900,41 @@ OPTION (RECOMPILE); UPDATE p SET p.trace_flags_session = tf.session_trace_flags -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p JOIN #trace_flags tf ON tf.QueryHash = p.QueryHash WHERE SPID = @@SPID -OPTION(RECOMPILE); +OPTION (RECOMPILE); + END; +RAISERROR(N'Checking for MSTVFs', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mstvf = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp[(@EstimateRows="100" or @EstimateRows="1") and @LogicalOp="Table-valued function"]') = 1 +OPTION (RECOMPILE); + + +IF @ExpertMode > 0 +BEGIN +RAISERROR(N'Checking for many to many merge joins', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE b +SET b.is_mm_join = 1 +FROM #relop AS r +JOIN ##BlitzCacheProcs AS b +ON b.SqlHandle = r.SqlHandle +WHERE r.relop.exist('/p:RelOp/p:Merge/@ManyToMany[.="1"]') = 1 +OPTION (RECOMPILE); +END ; + + +IF @ExpertMode > 0 +BEGIN RAISERROR(N'Is Paul White Electric?', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p), is_paul_white_electric AS ( @@ -3078,17 +3946,54 @@ WHERE c.n.exist('@PhysicalOp[.="Switch"]') = 1 ) UPDATE b SET b.is_paul_white_electric = ipwe.is_paul_white_electric -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b JOIN is_paul_white_electric ipwe ON ipwe.SqlHandle = b.SqlHandle WHERE b.SPID = @@SPID OPTION (RECOMPILE); +END ; -IF EXISTS ( SELECT 1 - FROM ##bou_BlitzCacheProcs AS bbcp - WHERE bbcp.implicit_conversions = 1 - OR bbcp.QueryType LIKE '%Procedure or Function: %') -BEGIN + +RAISERROR(N'Checking for non-sargable predicates', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) +, nsarg + AS ( SELECT r.QueryHash, 1 AS fn, 0 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator/p:Compare/p:ScalarOperator') AS ca(x) + WHERE ( ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName') = 1 + OR ca.x.exist('//p:ScalarOperator/p:IF') = 1 ) + UNION ALL + SELECT r.QueryHash, 0 AS fn, 1 AS jo, 0 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp//p:ScalarOperator') AS ca(x) + WHERE r.relop.exist('/p:RelOp[contains(@LogicalOp, "Join")]') = 1 + AND ca.x.exist('//p:ScalarOperator[contains(@ScalarString, "Expr")]') = 1 + UNION ALL + SELECT r.QueryHash, 0 AS fn, 0 AS jo, 1 AS lk + FROM #relop AS r + CROSS APPLY r.relop.nodes('/p:RelOp/p:IndexScan/p:Predicate/p:ScalarOperator') AS ca(x) + CROSS APPLY ca.x.nodes('//p:Const') AS co(x) + WHERE ca.x.exist('//p:ScalarOperator/p:Intrinsic/@FunctionName[.="like"]') = 1 + AND ( ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') <> 'N' + AND co.x.value('substring(@ConstValue, 2, 1)', 'VARCHAR(100)') = '%' ) + OR ( co.x.value('substring(@ConstValue, 1, 1)', 'VARCHAR(100)') = 'N' + AND co.x.value('substring(@ConstValue, 3, 1)', 'VARCHAR(100)') = '%' ))), + d_nsarg + AS ( SELECT DISTINCT + nsarg.QueryHash + FROM nsarg + WHERE nsarg.fn = 1 + OR nsarg.jo = 1 + OR nsarg.lk = 1 ) +UPDATE b +SET b.is_nonsargable = 1 +FROM d_nsarg AS d +JOIN ##BlitzCacheProcs AS b + ON b.QueryHash = d.QueryHash +WHERE b.SPID = @@SPID +OPTION ( RECOMPILE ); + +/*Begin implicit conversion and parameter info */ RAISERROR(N'Getting information about implicit conversions and stored proc parameters', 0, 1) WITH NOWAIT; @@ -3099,16 +4004,17 @@ SELECT DISTINCT @@SPID, qp.QueryHash, qp.SqlHandle, b.QueryType AS proc_name, - q.n.value('@Column', 'NVARCHAR(256)') AS variable_name, - q.n.value('@ParameterDataType', 'NVARCHAR(256)') AS variable_datatype, - q.n.value('@ParameterCompiledValue', 'NVARCHAR(4000)') AS compile_time_value + q.n.value('@Column', 'NVARCHAR(258)') AS variable_name, + q.n.value('@ParameterDataType', 'NVARCHAR(258)') AS variable_datatype, + q.n.value('@ParameterCompiledValue', 'NVARCHAR(258)') AS compile_time_value FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b +JOIN ##BlitzCacheProcs AS b ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:ParameterList/p:ColumnReference') AS q(n) WHERE b.SPID = @@SPID -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); + RAISERROR(N'Getting conversion info', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) @@ -3119,7 +4025,7 @@ SELECT DISTINCT @@SPID, b.QueryType AS proc_name, qq.c.value('@Expression', 'NVARCHAR(4000)') AS expression FROM #query_plan AS qp -JOIN ##bou_BlitzCacheProcs AS b +JOIN ##BlitzCacheProcs AS b ON (b.QueryType = 'adhoc' AND b.QueryHash = qp.QueryHash) OR (b.QueryType <> 'adhoc' AND b.SqlHandle = qp.SqlHandle) CROSS APPLY qp.query_plan.nodes('//p:QueryPlan/p:Warnings/p:PlanAffectingConvert') AS qq(c) @@ -3127,7 +4033,8 @@ WHERE qq.c.exist('@ConvertIssue[.="Seek Plan"]') = 1 AND qp.QueryHash IS NOT NULL AND b.implicit_conversions = 1 AND b.SPID = @@SPID -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); + RAISERROR(N'Parsing conversion info', 0, 1) WITH NOWAIT; INSERT #stored_proc_info ( SPID, SqlHandle, QueryHash, proc_name, variable_name, variable_datatype, converted_column_name, column_name, converted_to, compile_time_value ) @@ -3152,7 +4059,7 @@ SELECT @@SPID AS SPID, AND ci.convert_implicit_charindex = 0 THEN SUBSTRING(ci.expression, ci.equal_charindex, 4000) WHEN ci.at_charindex = 0 - AND ci.equal_charindex > 0 + AND (ci.equal_charindex -1) > 0 AND ci.convert_implicit_charindex > 0 THEN SUBSTRING(ci.expression, 0, ci.equal_charindex -1) WHEN ci.at_charindex > 0 @@ -3165,18 +4072,17 @@ 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 ); - +OPTION (RECOMPILE); -RAISERROR(N'Updating variables inserted procs', 0, 1) WITH NOWAIT; +RAISERROR(N'Updating variables for inserted procs', 0, 1) WITH NOWAIT; UPDATE sp SET sp.variable_datatype = vi.variable_datatype, sp.compile_time_value = vi.compile_time_value @@ -3185,7 +4091,7 @@ JOIN #variable_info AS vi ON (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) AND sp.variable_name = vi.variable_name -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); RAISERROR(N'Inserting variables for other procs', 0, 1) WITH NOWAIT; @@ -3200,44 +4106,75 @@ WHERE NOT EXISTS WHERE (sp.proc_name = 'adhoc' AND sp.QueryHash = vi.QueryHash) OR (sp.proc_name <> 'adhoc' AND sp.SqlHandle = vi.SqlHandle) ) -OPTION ( RECOMPILE ); +OPTION (RECOMPILE); RAISERROR(N'Updating procs', 0, 1) WITH NOWAIT; UPDATE s -SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' THEN - LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) +SET s.variable_datatype = CASE WHEN s.variable_datatype LIKE '%(%)%' + THEN LEFT(s.variable_datatype, CHARINDEX('(', s.variable_datatype) - 1) ELSE s.variable_datatype END, - s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' THEN - LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) + s.converted_to = CASE WHEN s.converted_to LIKE '%(%)%' + THEN LEFT(s.converted_to, CHARINDEX('(', s.converted_to) - 1) ELSE s.converted_to END, - s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' THEN - SUBSTRING(s.compile_time_value, - CHARINDEX('(', s.compile_time_value) + 1, - CHARINDEX(')', s.compile_time_value) - 1 - - CHARINDEX('(', s.compile_time_value) - ) + s.compile_time_value = CASE WHEN s.compile_time_value LIKE '%(%)%' + THEN SUBSTRING(s.compile_time_value, + CHARINDEX('(', s.compile_time_value) + 1, + CHARINDEX(')', s.compile_time_value, CHARINDEX('(', s.compile_time_value) + 1) - 1 - CHARINDEX('(', s.compile_time_value) + ) WHEN variable_datatype NOT IN ('bit', 'tinyint', 'smallint', 'int', 'bigint') - AND s.variable_datatype NOT LIKE '%binary%' - AND s.compile_time_value NOT LIKE 'N''%''' - AND s.compile_time_value NOT LIKE '''%''' THEN - QUOTENAME(compile_time_value, '''') + AND s.variable_datatype NOT LIKE '%binary%' + AND s.compile_time_value NOT LIKE 'N''%''' + AND s.compile_time_value NOT LIKE '''%''' + AND s.compile_time_value <> s.column_name + AND s.compile_time_value <> '**idk_man**' + THEN QUOTENAME(compile_time_value, '''') ELSE s.compile_time_value END FROM #stored_proc_info AS s +OPTION (RECOMPILE); + + +RAISERROR(N'Updating SET options', 0, 1) WITH NOWAIT; +WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) +UPDATE s +SET set_options = set_options.ansi_set_options +FROM #stored_proc_info AS s +JOIN ( + SELECT x.SqlHandle, + N'SET ANSI_NULLS ' + CASE WHEN [ANSI_NULLS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_PADDING ' + CASE WHEN [ANSI_PADDING] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ANSI_WARNINGS ' + CASE WHEN [ANSI_WARNINGS] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET ARITHABORT ' + CASE WHEN [ARITHABORT] = 'true' THEN N'ON ' ELSE N' OFF ' END + NCHAR(10) + + N'SET CONCAT_NULL_YIELDS_NULL ' + CASE WHEN [CONCAT_NULL_YIELDS_NULL] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET NUMERIC_ROUNDABORT ' + CASE WHEN [NUMERIC_ROUNDABORT] = 'true' THEN N'ON ' ELSE N'OFF ' END + NCHAR(10) + + N'SET QUOTED_IDENTIFIER ' + CASE WHEN [QUOTED_IDENTIFIER] = 'true' THEN N'ON ' ELSE N'OFF ' + NCHAR(10) END AS [ansi_set_options] + FROM ( + SELECT + s.SqlHandle, + so.o.value('@ANSI_NULLS', 'NVARCHAR(20)') AS [ANSI_NULLS], + so.o.value('@ANSI_PADDING', 'NVARCHAR(20)') AS [ANSI_PADDING], + so.o.value('@ANSI_WARNINGS', 'NVARCHAR(20)') AS [ANSI_WARNINGS], + so.o.value('@ARITHABORT', 'NVARCHAR(20)') AS [ARITHABORT], + so.o.value('@CONCAT_NULL_YIELDS_NULL', 'NVARCHAR(20)') AS [CONCAT_NULL_YIELDS_NULL], + so.o.value('@NUMERIC_ROUNDABORT', 'NVARCHAR(20)') AS [NUMERIC_ROUNDABORT], + so.o.value('@QUOTED_IDENTIFIER', 'NVARCHAR(20)') AS [QUOTED_IDENTIFIER] + FROM #statements AS s + CROSS APPLY s.statement.nodes('//p:StatementSetOptions') AS so(o) + ) AS x +) AS set_options ON set_options.SqlHandle = s.SqlHandle OPTION(RECOMPILE); + RAISERROR(N'Updating conversion XML', 0, 1) WITH NOWAIT; WITH precheck AS ( SELECT spi.SPID, spi.SqlHandle, spi.proc_name, - CONVERT(XML, - N' 'Statement' + (SELECT + CASE WHEN spi.proc_name <> 'Statement' THEN N'The stored procedure ' + spi.proc_name ELSE N'This ad hoc statement' END @@ -3258,7 +4195,6 @@ SELECT spi.SPID, THEN spi2.variable_name WHEN spi2.variable_name = N'**no_variable**' AND (spi2.column_name = spi2.converted_column_name OR spi2.column_name LIKE '%CONVERT_IMPLICIT%') THEN spi2.compile_time_value - ELSE spi2.column_name END + N' has a data type of ' @@ -3281,6 +4217,7 @@ SELECT spi.SPID, WHEN spi2.column_name LIKE '%Expr%' THEN N'' WHEN spi2.compile_time_value NOT IN ('**declared in proc**', '**idk_man**') + AND spi2.compile_time_value <> spi2.column_name THEN ' with the value ' + RTRIM(spi2.compile_time_value) ELSE N'' END @@ -3288,27 +4225,28 @@ SELECT spi.SPID, FROM #stored_proc_info AS spi2 WHERE spi.SqlHandle = spi2.SqlHandle FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + CHAR(10) - + N' -- ?>' - ) AS implicit_conversion_info + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS implicit_conversion_info FROM #stored_proc_info AS spi GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name ) UPDATE b SET b.implicit_conversion_info = pk.implicit_conversion_info -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b JOIN precheck pk ON pk.SqlHandle = b.SqlHandle AND pk.SPID = b.SPID -OPTION(RECOMPILE); +OPTION (RECOMPILE); -RAISERROR(N'Updating cached parameter XML', 0, 1) WITH NOWAIT; + +RAISERROR(N'Updating cached parameter XML for stored procs', 0, 1) WITH NOWAIT; WITH precheck AS ( SELECT spi.SPID, spi.SqlHandle, spi.proc_name, -CONVERT(XML, - N' N'Statement' FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') - + @nl - + N' -- ?>' - ) AS cached_execution_parameters + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters FROM #stored_proc_info AS spi -GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options ) UPDATE b SET b.cached_execution_parameters = pk.cached_execution_parameters -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b JOIN precheck pk ON pk.SqlHandle = b.SqlHandle AND pk.SPID = b.SPID -OPTION(RECOMPILE); +WHERE b.QueryType <> N'Statement' +OPTION (RECOMPILE); -END; --End implicit conversion information gathering +RAISERROR(N'Updating cached parameter XML for statements', 0, 1) WITH NOWAIT; +WITH precheck AS ( +SELECT spi.SPID, + spi.SqlHandle, + spi.proc_name, + (SELECT + set_options + + @nl + + @nl + + N' See QueryText column for full query text' + + @nl + + @nl + + STUFF(( + SELECT DISTINCT N', ' + + CASE WHEN spi2.variable_name <> N'**no_variable**' AND spi2.compile_time_value <> N'**idk_man**' + THEN spi2.variable_name + N' = ' + ELSE @nl + N' We could not find any cached parameter values for this stored proc. ' + END + + CASE WHEN spi2.variable_name = N'**no_variable**' OR spi2.compile_time_value = N'**idk_man**' + THEN @nl + N' More info on possible reasons: https://www.brentozar.com/go/noplans ' + WHEN spi2.compile_time_value = N'NULL' + THEN spi2.compile_time_value + ELSE RTRIM(spi2.compile_time_value) + END + FROM #stored_proc_info AS spi2 + WHERE spi.SqlHandle = spi2.SqlHandle + AND spi2.proc_name = N'Statement' + AND spi2.variable_name NOT LIKE N'%msparam%' + FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + AS [processing-instruction(ClickMe)] FOR XML PATH(''), TYPE ) + AS cached_execution_parameters +FROM #stored_proc_info AS spi +GROUP BY spi.SPID, spi.SqlHandle, spi.proc_name, spi.set_options +) +UPDATE b +SET b.cached_execution_parameters = pk.cached_execution_parameters +FROM ##BlitzCacheProcs AS b +JOIN precheck pk +ON pk.SqlHandle = b.SqlHandle +AND pk.SPID = b.SPID +WHERE b.QueryType = N'Statement' +OPTION (RECOMPILE); +RAISERROR(N'Filling in implicit conversion and cached plan parameter info', 0, 1) WITH NOWAIT; UPDATE b -SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL THEN '' ELSE b.implicit_conversion_info END, - b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL THEN '' ELSE b.cached_execution_parameters END -FROM ##bou_BlitzCacheProcs AS b +SET b.implicit_conversion_info = CASE WHEN b.implicit_conversion_info IS NULL + OR CONVERT(NVARCHAR(MAX), b.implicit_conversion_info) = N'' + THEN '' + ELSE b.implicit_conversion_info END, + b.cached_execution_parameters = CASE WHEN b.cached_execution_parameters IS NULL + OR CONVERT(NVARCHAR(MAX), b.cached_execution_parameters) = N'' + THEN '' + ELSE b.cached_execution_parameters END +FROM ##BlitzCacheProcs AS b WHERE b.SPID = @@SPID -OPTION(RECOMPILE); +OPTION (RECOMPILE); -/*Begin Missing Index*/ +/*End implicit conversion and parameter info*/ -IF EXISTS - (SELECT 1 FROM ##bou_BlitzCacheProcs AS bbcp WHERE bbcp.missing_index_count > 0 AND bbcp.SPID = @@SPID) - BEGIN - +/*Begin Missing Index*/ +IF EXISTS ( SELECT 1/0 + FROM ##BlitzCacheProcs AS bbcp + WHERE bbcp.missing_index_count > 0 + OR bbcp.index_spool_cost > 0 + OR bbcp.index_spool_rows > 0 + AND bbcp.SPID = @@SPID ) + + BEGIN + RAISERROR(N'Inserting to #missing_index_xml', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #missing_index_xml SELECT qp.QueryHash, @@ -3368,20 +4360,21 @@ IF EXISTS FROM #query_plan AS qp CROSS APPLY qp.query_plan.nodes('//p:MissingIndexes/p:MissingIndexGroup') AS c(mg) WHERE qp.QueryHash IS NOT NULL - AND c.mg.value('@Impact', 'FLOAT') > 70.0 OPTION(RECOMPILE); + RAISERROR(N'Inserting to #missing_index_schema', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #missing_index_schema SELECT mix.QueryHash, mix.SqlHandle, mix.impact, - c.mi.value('@Database', 'NVARCHAR(128)') , - c.mi.value('@Schema', 'NVARCHAR(128)') , - c.mi.value('@Table', 'NVARCHAR(128)') , + c.mi.value('@Database', 'NVARCHAR(128)'), + c.mi.value('@Schema', 'NVARCHAR(128)'), + c.mi.value('@Table', 'NVARCHAR(128)'), c.mi.query('.') FROM #missing_index_xml AS mix CROSS APPLY mix.index_xml.nodes('//p:MissingIndex') AS c(mi) OPTION(RECOMPILE); + RAISERROR(N'Inserting to #missing_index_usage', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #missing_index_usage SELECT ms.QueryHash, ms.SqlHandle, ms.impact, ms.database_name, ms.schema_name, ms.table_name, @@ -3391,6 +4384,7 @@ IF EXISTS CROSS APPLY ms.index_xml.nodes('//p:ColumnGroup') AS c(cg) OPTION(RECOMPILE); + RAISERROR(N'Inserting to #missing_index_detail', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES ( 'http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p ) INSERT #missing_index_detail SELECT miu.QueryHash, @@ -3403,10 +4397,12 @@ IF EXISTS c.c.value('@Name', 'NVARCHAR(128)') FROM #missing_index_usage AS miu CROSS APPLY miu.index_xml.nodes('//p:Column') AS c(c) - OPTION(RECOMPILE); + OPTION (RECOMPILE); - INSERT #missing_index_pretty - SELECT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name + RAISERROR(N'Inserting to missing indexes to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + ( QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool ) + SELECT DISTINCT m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'EQUALITY' @@ -3416,7 +4412,7 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS equality + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'INEQUALITY' @@ -3426,7 +4422,7 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS inequality + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality , STUFF(( SELECT DISTINCT N', ' + ISNULL(m2.column_name, '') AS column_name FROM #missing_index_detail AS m2 WHERE m2.usage = 'INCLUDE' @@ -3436,62 +4432,137 @@ IF EXISTS AND m.database_name = m2.database_name AND m.schema_name = m2.schema_name AND m.table_name = m2.table_name - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), 1, 2, N'') AS [include] + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS [include], + bbcp.ExecutionCount, + bbcp.QueryPlanCost, + bbcp.PlanCreationTimeHours, + 0 as is_spool FROM #missing_index_detail AS m - GROUP BY m.QueryHash, m.SqlHandle, m.impact, m.database_name, m.schema_name, m.table_name - OPTION(RECOMPILE); + JOIN ##BlitzCacheProcs AS bbcp + ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + OPTION (RECOMPILE); + RAISERROR(N'Inserting to #index_spool_ugly', 0, 1) WITH NOWAIT; + WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) + INSERT #index_spool_ugly + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours) + SELECT p.QueryHash, + p.SqlHandle, + (c.n.value('@EstimateIO', 'FLOAT') + (c.n.value('@EstimateCPU', 'FLOAT'))) + / ( 1 * NULLIF(p.QueryPlanCost, 0)) * 100 AS impact, + o.n.value('@Database', 'NVARCHAR(128)') AS output_database, + o.n.value('@Schema', 'NVARCHAR(128)') AS output_schema, + o.n.value('@Table', 'NVARCHAR(128)') AS output_table, + k.n.value('@Column', 'NVARCHAR(128)') AS range_column, + e.n.value('@Column', 'NVARCHAR(128)') AS expression_column, + o.n.value('@Column', 'NVARCHAR(128)') AS output_column, + p.ExecutionCount, + p.QueryPlanCost, + p.PlanCreationTimeHours + FROM #relop AS r + JOIN ##BlitzCacheProcs p + ON p.QueryHash = r.QueryHash + CROSS APPLY r.relop.nodes('/p:RelOp') AS c(n) + CROSS APPLY r.relop.nodes('/p:RelOp/p:OutputList/p:ColumnReference') AS o(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeColumns/p:ColumnReference') AS k(n) + OUTER APPLY r.relop.nodes('/p:RelOp/p:Spool/p:SeekPredicateNew/p:SeekKeys/p:Prefix/p:RangeExpressions/p:ColumnReference') AS e(n) + WHERE r.relop.exist('/p:RelOp[@PhysicalOp="Index Spool" and @LogicalOp="Eager Spool"]') = 1 + + RAISERROR(N'Inserting to spools to #missing_index_pretty', 0, 1) WITH NOWAIT; + INSERT #missing_index_pretty + (QueryHash, SqlHandle, impact, database_name, schema_name, table_name, equality, inequality, include, executions, query_cost, creation_hours, is_spool) + SELECT DISTINCT + isu.QueryHash, + isu.SqlHandle, + isu.impact, + isu.database_name, + isu.schema_name, + isu.table_name + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.equality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.equality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS equality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.inequality, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.inequality IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS inequality + , STUFF(( SELECT DISTINCT N', ' + ISNULL(isu2.include, '') AS column_name + FROM #index_spool_ugly AS isu2 + WHERE isu2.include IS NOT NULL + AND isu.QueryHash = isu2.QueryHash + AND isu.SqlHandle = isu2.SqlHandle + AND isu.impact = isu2.impact + AND isu.database_name = isu2.database_name + AND isu.schema_name = isu2.schema_name + AND isu.table_name = isu2.table_name + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 2, N'') AS include, + isu.executions, + isu.query_cost, + isu.creation_hours, + 1 AS is_spool + FROM #index_spool_ugly AS isu + + + RAISERROR(N'Updating missing index information', 0, 1) WITH NOWAIT; WITH missing AS ( - SELECT mip.QueryHash, + SELECT DISTINCT + mip.QueryHash, mip.SqlHandle, - CONVERT(XML, - N'' - ) AS full_details + + N']]>' + AS full_details FROM #missing_index_pretty AS mip - GROUP BY mip.QueryHash, mip.SqlHandle, mip.impact ) UPDATE bbcp SET bbcp.missing_indexes = m.full_details - FROM ##bou_BlitzCacheProcs AS bbcp + FROM ##BlitzCacheProcs AS bbcp JOIN missing AS m ON m.SqlHandle = bbcp.SqlHandle + AND m.QueryHash = bbcp.QueryHash + AND m.executions = bbcp.ExecutionCount AND SPID = @@SPID - OPTION(RECOMPILE); + OPTION (RECOMPILE); - - END + END; + RAISERROR(N'Filling in missing index blanks', 0, 1) WITH NOWAIT; UPDATE b SET b.missing_indexes = CASE WHEN b.missing_indexes IS NULL THEN '' ELSE b.missing_indexes END - FROM ##bou_BlitzCacheProcs AS b + FROM ##BlitzCacheProcs AS b WHERE b.SPID = @@SPID - OPTION(RECOMPILE); + OPTION (RECOMPILE); /*End Missing Index*/ - -IF @SkipAnalysis = 1 - BEGIN - RAISERROR(N'Skipping analysis, going to results', 0, 1) WITH NOWAIT; - GOTO Results ; - END; - - /* Set configuration values */ RAISERROR(N'Setting configuration values', 0, 1) WITH NOWAIT; DECLARE @execution_threshold INT = 1000 , @@ -3580,32 +4651,34 @@ OPTION (RECOMPILE); RAISERROR('Checking for query level SQL Server issues.', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET frequent_execution = CASE WHEN ExecutionsPerMinute > @execution_threshold THEN 1 END , - parameter_sniffing = CASE WHEN AverageReads > @parameter_sniffing_io_threshold + parameter_sniffing = CASE WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold AND min_worker_time < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold AND max_worker_time > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageCPU) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold AND MinReturnedRows < ((1.0 - (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 - WHEN AverageReads > @parameter_sniffing_io_threshold + WHEN ExecutionCount > 3 AND AverageReads > @parameter_sniffing_io_threshold AND MaxReturnedRows > ((1.0 + (@parameter_sniffing_warning_pct / 100.0)) * AverageReturnedRows) THEN 1 END , - near_parallel = CASE WHEN QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, + near_parallel = CASE WHEN is_parallel <> 1 AND QueryPlanCost BETWEEN @ctp * (1 - (@ctp_threshold_pct / 100.0)) AND @ctp THEN 1 END, long_running = CASE WHEN AverageDuration > @long_running_query_warning_seconds THEN 1 WHEN max_worker_time > @long_running_query_warning_seconds THEN 1 WHEN max_elapsed_time > @long_running_query_warning_seconds THEN 1 END, - is_key_lookup_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, - is_sort_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, + is_key_lookup_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND key_lookup_cost >= QueryPlanCost * .5 THEN 1 END, + is_sort_expensive = CASE WHEN QueryPlanCost >= (@ctp / 2) AND sort_cost >= QueryPlanCost * .5 THEN 1 END, is_remote_query_expensive = CASE WHEN remote_query_cost >= QueryPlanCost * .05 THEN 1 END, - is_forced_serial = CASE WHEN is_forced_serial = 1 THEN 1 END, is_unused_grant = CASE WHEN PercentMemoryGrantUsed <= @memory_grant_warning_percent AND MinGrantKB > @MinMemoryPerQuery THEN 1 END, - long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 THEN 1 END, - low_cost_high_cpu = CASE WHEN QueryPlanCost < @ctp AND AverageCPU > 500. AND QueryPlanCost * 10 < AverageCPU THEN 1 END, - is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 2) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, + long_running_low_cpu = CASE WHEN AverageDuration > AverageCPU * 4 AND AverageCPU < 500. THEN 1 END, + low_cost_high_cpu = CASE WHEN QueryPlanCost <= 10 AND AverageCPU > 5000. THEN 1 END, + is_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND index_spool_cost >= QueryPlanCost * .1 THEN 1 END, is_spool_more_rows = CASE WHEN index_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, - is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 1000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 1000) THEN 1 END + is_table_spool_expensive = CASE WHEN QueryPlanCost > (@ctp / 5) AND table_spool_cost >= QueryPlanCost / 4 THEN 1 END, + is_table_spool_more_rows = CASE WHEN table_spool_rows >= (AverageReturnedRows / ISNULL(NULLIF(ExecutionCount, 0), 1)) THEN 1 END, + is_bad_estimate = CASE WHEN AverageReturnedRows > 0 AND (estimated_rows * 1000 < AverageReturnedRows OR estimated_rows > AverageReturnedRows * 1000) THEN 1 END, + is_big_spills = CASE WHEN (AvgSpills / 128.) > 499. THEN 1 END WHERE SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); @@ -3624,27 +4697,35 @@ UPDATE p CASE WHEN (CAST(pa.value AS INT) & 4096 = 4096) THEN ', ARITH_ABORT' ELSE '' END + CASE WHEN (CAST(pa.value AS INT) & 8192 = 8191) THEN ', NUMERIC_ROUNDABORT' ELSE '' END , 2, 200000) -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa WHERE pa.attribute = 'set_options' AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); /* Cursor checks */ UPDATE p SET is_cursor = CASE WHEN CAST(pa.value AS INT) <> 0 THEN 1 END -FROM ##bou_BlitzCacheProcs p +FROM ##BlitzCacheProcs p CROSS APPLY sys.dm_exec_plan_attributes(p.PlanHandle) pa WHERE pa.attribute LIKE '%cursor%' AND SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); + +UPDATE p +SET is_cursor = 1 +FROM ##BlitzCacheProcs p +WHERE QueryHash = 0x0000000000000000 +OR QueryPlanHash = 0x0000000000000000 +AND SPID = @@SPID +OPTION (RECOMPILE); RAISERROR('Populating Warnings column', 0, 1) WITH NOWAIT; /* Populate warnings */ -UPDATE ##bou_BlitzCacheProcs +UPDATE ##BlitzCacheProcs SET Warnings = SUBSTRING( CASE WHEN warning_no_join_predicate = 1 THEN ', No Join Predicate' ELSE '' END + CASE WHEN compile_timeout = 1 THEN ', Compilation Timeout' ELSE '' END + @@ -3656,8 +4737,10 @@ SET Warnings = SUBSTRING( CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CAST(missing_index_count AS VARCHAR(3)) + ')' ELSE '' END + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CAST(unmatched_index_count AS VARCHAR(3)) + ')' ELSE '' END + CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END ELSE '' END + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + @@ -3668,20 +4751,20 @@ SET Warnings = SUBSTRING( CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR function(s)' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), function_count) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), clr_function_count) + ' CLR Function(s)' ELSE '' END + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + CASE WHEN relop_warnings = 1 THEN ', Operator Warnings' ELSE '' END + - CASE WHEN is_table_scan = 1 THEN ', Table Scans' ELSE '' END + + CASE WHEN is_table_scan = 1 THEN ', Table Scans (Heaps)' ELSE '' END + CASE WHEN backwards_scan = 1 THEN ', Backwards Scans' ELSE '' END + CASE WHEN forced_index = 1 THEN ', Forced Indexes' ELSE '' END + CASE WHEN forced_seek = 1 THEN ', Forced Seeks' ELSE '' END + @@ -3701,11 +4784,22 @@ SET Warnings = SUBSTRING( CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb Spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) WHERE SPID = @@SPID -OPTION (RECOMPILE) ; +OPTION (RECOMPILE); RAISERROR('Populating Warnings column for stored procedures', 0, 1) WITH NOWAIT; @@ -3721,11 +4815,13 @@ SELECT DISTINCT CASE WHEN is_forced_plan = 1 THEN ', Forced Plan' ELSE '' END + CASE WHEN is_forced_parameterized = 1 THEN ', Forced Parameterization' ELSE '' END + --CASE WHEN unparameterized_query = 1 THEN ', Unparameterized Query' ELSE '' END + - CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + - CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ')' ELSE '' END + + CASE WHEN missing_index_count > 0 THEN ', Missing Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.missing_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + + CASE WHEN unmatched_index_count > 0 THEN ', Unmatched Indexes (' + CONVERT(VARCHAR(10), (SELECT SUM(b2.unmatched_index_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ')' ELSE '' END + CASE WHEN is_cursor = 1 THEN ', Cursor' - + CASE WHEN is_optimistic_cursor = 1 THEN ' with optimistic' ELSE '' END - + CASE WHEN is_forward_only_cursor = 0 THEN ' not forward only' ELSE '' END + + CASE WHEN is_optimistic_cursor = 1 THEN '; optimistic' ELSE '' END + + CASE WHEN is_forward_only_cursor = 0 THEN '; not forward only' ELSE '' END + + CASE WHEN is_cursor_dynamic = 1 THEN '; dynamic' ELSE '' END + + CASE WHEN is_fast_forward_cursor = 1 THEN '; fast forward' ELSE '' END ELSE '' END + CASE WHEN is_parallel = 1 THEN ', Parallel' ELSE '' END + CASE WHEN near_parallel = 1 THEN ', Nearly Parallel' ELSE '' END + @@ -3736,15 +4832,15 @@ SELECT DISTINCT CASE WHEN downlevel_estimator = 1 THEN ', Downlevel CE' ELSE '' END + CASE WHEN implicit_conversions = 1 THEN ', Implicit Conversions' ELSE '' END + CASE WHEN tvf_join = 1 THEN ', Function Join' ELSE '' END + - CASE WHEN plan_multiple_plans = 1 THEN ', Multiple Plans' ELSE '' END + + CASE WHEN plan_multiple_plans > 0 THEN ', Multiple Plans' + COALESCE(' (' + CAST(plan_multiple_plans AS VARCHAR(10)) + ')', '') ELSE '' END + CASE WHEN is_trivial = 1 THEN ', Trivial Plans' ELSE '' END + CASE WHEN is_forced_serial = 1 THEN ', Forced Serialization' ELSE '' END + CASE WHEN is_key_lookup_expensive = 1 THEN ', Expensive Key Lookup' ELSE '' END + CASE WHEN is_remote_query_expensive = 1 THEN ', Expensive Remote Query' ELSE '' END + CASE WHEN trace_flags_session IS NOT NULL THEN ', Session Level Trace Flag(s) Enabled: ' + trace_flags_session ELSE '' END + CASE WHEN is_unused_grant = 1 THEN ', Unused Memory Grant' ELSE '' END + - CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' function(s)' ELSE '' END + - CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##bou_BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL) ) + ' CLR function(s)' ELSE '' END + + CASE WHEN function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' Function(s)' ELSE '' END + + CASE WHEN clr_function_count > 0 THEN ', Calls ' + CONVERT(VARCHAR(10), (SELECT SUM(b2.clr_function_count) FROM ##BlitzCacheProcs AS b2 WHERE b2.SqlHandle = b.SqlHandle AND b2.QueryHash IS NOT NULL AND SPID = @@SPID) ) + ' CLR Function(s)' ELSE '' END + CASE WHEN PlanCreationTimeHours <= 4 THEN ', Plan created last 4hrs' ELSE '' END + CASE WHEN is_table_variable = 1 THEN ', Table Variables' ELSE '' END + CASE WHEN no_stats_warning = 1 THEN ', Columns With No Statistics' ELSE '' END + @@ -3769,26 +4865,37 @@ SELECT DISTINCT CASE WHEN is_adaptive = 1 THEN + ', Adaptive Joins' ELSE '' END + CASE WHEN is_spool_expensive = 1 THEN + ', Expensive Index Spool' ELSE '' END + CASE WHEN is_spool_more_rows = 1 THEN + ', Large Index Row Spool' ELSE '' END + - CASE WHEN is_bad_estimate = 1 THEN + ', Row estimate mismatch' ELSE '' END + - CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END - , 2, 200000) -FROM ##bou_BlitzCacheProcs b + CASE WHEN is_table_spool_expensive = 1 THEN + ', Expensive Table Spool' ELSE '' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + ', Many Rows Table Spool' ELSE '' END + + CASE WHEN is_bad_estimate = 1 THEN + ', Row Estimate Mismatch' ELSE '' END + + CASE WHEN is_paul_white_electric = 1 THEN ', SWITCH!' ELSE '' END + + CASE WHEN is_row_goal = 1 THEN ', Row Goals' ELSE '' END + + CASE WHEN is_big_spills = 1 THEN ', >500mb spills' ELSE '' END + + CASE WHEN is_mstvf = 1 THEN ', MSTVFs' ELSE '' END + + CASE WHEN is_mm_join = 1 THEN ', Many to Many Merge' ELSE '' END + + CASE WHEN is_nonsargable = 1 THEN ', non-SARGables' ELSE '' END + + CASE WHEN CompileTime > 5000 THEN ', Long Compile Time' ELSE '' END + + CASE WHEN CompileCPU > 5000 THEN ', High Compile CPU' ELSE '' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN ', High Compile Memory' ELSE '' END + + CASE WHEN select_with_writes > 0 THEN ', Select w/ Writes' ELSE '' END + , 3, 200000) +FROM ##BlitzCacheProcs b WHERE SPID = @@SPID AND QueryType LIKE 'Statement (parent%' ) UPDATE b SET b.Warnings = s.Warnings -FROM ##bou_BlitzCacheProcs AS b +FROM ##BlitzCacheProcs AS b JOIN statement_warnings s ON b.SqlHandle = s.SqlHandle WHERE QueryType LIKE 'Procedure or Function%' AND SPID = @@SPID -OPTION(RECOMPILE); +OPTION (RECOMPILE); RAISERROR('Checking for plans with >128 levels of nesting', 0, 1) WITH NOWAIT; WITH plan_handle AS ( SELECT b.PlanHandle -FROM ##bou_BlitzCacheProcs b +FROM ##BlitzCacheProcs b CROSS APPLY sys.dm_exec_text_query_plan(b.PlanHandle, 0, -1) tqp CROSS APPLY sys.dm_exec_query_plan(b.PlanHandle) qp WHERE tqp.encrypted = 0 @@ -3798,8 +4905,8 @@ FROM ##bou_BlitzCacheProcs b ) UPDATE b SET Warnings = ISNULL('Your query plan is >128 levels of nested nodes, and can''t be converted to XML. Use SELECT * FROM sys.dm_exec_text_query_plan('+ CONVERT(VARCHAR(128), ph.PlanHandle, 1) + ', 0, -1) to get more information' - , 'We couldn''t find a plan for this query. Possible reasons for this include dynamic SQL, RECOMPILE hints, and encrypted code.') -FROM ##bou_BlitzCacheProcs b + , 'We couldn''t find a plan for this query. More info on possible reasons: https://www.brentozar.com/go/noplans') +FROM ##BlitzCacheProcs b LEFT JOIN plan_handle ph ON b.PlanHandle = ph.PlanHandle WHERE b.QueryPlan IS NULL @@ -3807,208 +4914,29 @@ AND b.SPID = @@SPID OPTION (RECOMPILE); RAISERROR('Checking for plans with no warnings', 0, 1) WITH NOWAIT; -UPDATE ##bou_BlitzCacheProcs -SET Warnings = 'No warnings detected.' +UPDATE ##BlitzCacheProcs +SET Warnings = 'No warnings detected. ' + CASE @ExpertMode + WHEN 0 + THEN ' Try running sp_BlitzCache with @ExpertMode = 1 to find more advanced problems.' + ELSE '' + END WHERE Warnings = '' OR Warnings IS NULL AND SPID = @@SPID OPTION (RECOMPILE); Results: -IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL -BEGIN - RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; - - /* send results to a table */ - DECLARE @insert_sql NVARCHAR(MAX) = N'' ; - - SET @insert_sql = 'USE ' - + @OutputDatabaseName - + '; IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName - + ''') AND NOT EXISTS (SELECT * FROM ' - + @OutputDatabaseName - + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' - + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' - + @OutputTableName + ''') CREATE TABLE ' - + @OutputSchemaName + '.' - + @OutputTableName - + N'(ID bigint NOT NULL IDENTITY(1,1), - ServerName nvarchar(256), - CheckDate DATETIMEOFFSET, - Version nvarchar(256), - QueryType nvarchar(256), - Warnings varchar(max), - DatabaseName sysname, - SerialDesiredMemory float, - SerialRequiredMemory float, - AverageCPU bigint, - TotalCPU bigint, - PercentCPUByType money, - CPUWeight money, - AverageDuration bigint, - TotalDuration bigint, - DurationWeight money, - PercentDurationByType money, - AverageReads bigint, - TotalReads bigint, - ReadWeight money, - PercentReadsByType money, - AverageWrites bigint, - TotalWrites bigint, - WriteWeight money, - PercentWritesByType money, - ExecutionCount bigint, - ExecutionWeight money, - PercentExecutionsByType money,' + N' - ExecutionsPerMinute money, - PlanCreationTime datetime, - PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), - LastExecutionTime datetime, - PlanHandle varbinary(64), - [Remove Plan Handle From Cache] AS - CASE WHEN [PlanHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' - ELSE ''N/A'' END, - SqlHandle varbinary(64), - [Remove SQL Handle From Cache] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' - ELSE ''N/A'' END, - [SQL Handle More Info] AS - CASE WHEN [SqlHandle] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryHash binary(8), - [Query Hash More Info] AS - CASE WHEN [QueryHash] IS NOT NULL - THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' - ELSE ''N/A'' END, - QueryPlanHash binary(8), - StatementStartOffset int, - StatementEndOffset int, - MinReturnedRows bigint, - MaxReturnedRows bigint, - AverageReturnedRows money, - TotalReturnedRows bigint, - QueryText nvarchar(max), - QueryPlan xml, - NumberOfPlans int, - NumberOfDistinctPlans int, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, - PercentMemoryGrantUsed MONEY, - AvgMaxMemoryGrant MONEY, - QueryPlanCost FLOAT, - CONSTRAINT [PK_' +CAST(NEWID() AS NCHAR(36)) + '] PRIMARY KEY CLUSTERED(ID))'; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql ; - - IF @CheckDateOverride IS NULL - BEGIN - SET @CheckDateOverride = SYSDATETIMEOFFSET(); - END - - - SET @insert_sql =N' IF EXISTS(SELECT * FROM ' - + @OutputDatabaseName - + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + N''') ' - + 'INSERT ' - + @OutputDatabaseName + '.' - + @OutputSchemaName + '.' - + @OutputTableName - + N' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ) ' - + N'SELECT TOP (@Top) ' - + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), N'''') + N', @CheckDateOverride, ' - + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), N'''') + ', ' - + N' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' - + N' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' - + N' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, QueryHash, StatementStartOffset, StatementEndOffset, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' - + N' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, QueryPlanCost ' - + N' FROM ##bou_BlitzCacheProcs ' - + N' WHERE 1=1 '; - - IF @MinimumExecutionCount IS NOT NULL - BEGIN - SET @insert_sql += N' AND ExecutionCount >= @MinimumExecutionCount '; - END; - - IF @MinutesBack IS NOT NULL - BEGIN - SET @insert_sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; - END; - - SET @insert_sql += N' AND SPID = @@SPID '; - - SELECT @insert_sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' - END + N' DESC '; - - SET @insert_sql += N' OPTION (RECOMPILE) ; '; - - IF @Debug = 1 - BEGIN - PRINT SUBSTRING(@insert_sql, 0, 4000); - PRINT SUBSTRING(@insert_sql, 4000, 8000); - PRINT SUBSTRING(@insert_sql, 8000, 12000); - PRINT SUBSTRING(@insert_sql, 12000, 16000); - PRINT SUBSTRING(@insert_sql, 16000, 20000); - PRINT SUBSTRING(@insert_sql, 20000, 24000); - PRINT SUBSTRING(@insert_sql, 24000, 28000); - PRINT SUBSTRING(@insert_sql, 28000, 32000); - PRINT SUBSTRING(@insert_sql, 32000, 36000); - PRINT SUBSTRING(@insert_sql, 36000, 40000); - END; - - EXEC sp_executesql @insert_sql, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; - - RETURN; -END; -ELSE IF @ExportToExcel = 1 +IF @ExportToExcel = 1 BEGIN RAISERROR('Displaying results with Excel formatting (no plans).', 0, 1) WITH NOWAIT; /* excel output */ - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),' ','<>'),'><',''),'<>',' '), 1, 32000) OPTION(RECOMPILE); SET @sql = N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT TOP (@Top) DatabaseName AS [Database Name], QueryPlanCost AS [Cost], @@ -4047,50 +4975,65 @@ BEGIN MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, + MinSpills, + MaxSpills, + TotalSpills, + AvgSpills, NumberOfPlans, NumberOfDistinctPlans, PlanCreationTime AS [Created At], LastExecutionTime AS [Last Execution], StatementStartOffset, StatementEndOffset, + PlanGenerationNum, PlanHandle AS [Plan Handle], SqlHandle AS [SQL Handle], QueryHash, QueryPlanHash, COALESCE(SetOptions, '''') AS [SET Options] - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE 1 = 1 AND SPID = @@SPID ' + @nl; IF @MinimumExecutionCount IS NOT NULL BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount '; + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount '; END; IF @MinutesBack IS NOT NULL BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; END; - SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN ' TotalCPU ' - WHEN 'reads' THEN ' TotalReads ' - WHEN 'writes' THEN ' TotalWrites ' - WHEN 'duration' THEN ' TotalDuration ' - WHEN 'executions' THEN ' ExecutionCount ' - WHEN 'compiles' THEN ' PlanCreationTime ' - WHEN 'memory grant' THEN 'MaxGrantKB' - WHEN 'avg cpu' THEN 'AverageCPU' - WHEN 'avg reads' THEN 'AverageReads' - WHEN 'avg writes' THEN 'AverageWrites' - WHEN 'avg duration' THEN 'AverageDuration' - WHEN 'avg executions' THEN 'ExecutionsPerMinute' - WHEN 'avg memory grant' THEN 'AvgMaxMemoryGrant' + SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'duplicate' THEN N' plan_multiple_plans ' /* Issue #3345 */ + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' END + N' DESC '; SET @sql += N' OPTION (RECOMPILE) ; '; + IF @sql IS NULL + BEGIN + RAISERROR('@sql is null, which means dynamic SQL generation went terribly wrong', 0, 1) WITH NOWAIT; + END + IF @Debug = 1 BEGIN + RAISERROR('Printing @sql, the dynamic SQL we generated:', 0, 1) WITH NOWAIT; PRINT SUBSTRING(@sql, 0, 4000); PRINT SUBSTRING(@sql, 4000, 8000); PRINT SUBSTRING(@sql, 8000, 12000); @@ -4103,7 +5046,7 @@ BEGIN PRINT SUBSTRING(@sql, 36000, 40000); END; - EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @minimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; + EXEC sp_executesql @sql, N'@Top INT, @min_duration INT, @min_back INT, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @MinimumExecutionCount; END; @@ -4123,32 +5066,40 @@ BEGIN missing_indexes AS [Missing Indexes], implicit_conversion_info AS [Implicit Conversion Info], cached_execution_parameters AS [Cached Execution Parameters], - ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Avg Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Avg Writes], - PercentWrites AS [Write Weight], - AverageReturnedRows AS [Average Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Avg Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Avg Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Average Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], PlanCreationTime AS [Created At], LastExecutionTime AS [Last Execution], + LastCompletionTime AS [Last Completion], PlanHandle AS [Plan Handle], SqlHandle AS [SQL Handle], - COALESCE(SetOptions, '''') AS [SET Options] '; + COALESCE(SetOptions, '''') AS [SET Options], + QueryHash AS [Query Hash], + PlanGenerationNum, + [Remove Plan Handle From Cache]'; END; ELSE BEGIN @@ -4186,7 +5137,7 @@ BEGIN CASE WHEN downlevel_estimator = 1 THEN '', 13'' ELSE '''' END + CASE WHEN implicit_conversions = 1 THEN '', 14'' ELSE '''' END + CASE WHEN tvf_join = 1 THEN '', 17'' ELSE '''' END + - CASE WHEN plan_multiple_plans = 1 THEN '', 21'' ELSE '''' END + + CASE WHEN plan_multiple_plans > 0 THEN '', 21'' ELSE '''' END + CASE WHEN unmatched_index_count > 0 THEN '', 22'' ELSE '''' END + CASE WHEN is_trivial = 1 THEN '', 24'' ELSE '''' END + CASE WHEN is_forced_serial = 1 THEN '', 25'' ELSE '''' END + @@ -4219,50 +5170,67 @@ BEGIN CASE WHEN is_adaptive = 1 THEN '', 53'' ELSE '''' END + CASE WHEN is_spool_expensive = 1 THEN + '', 54'' ELSE '''' END + CASE WHEN is_spool_more_rows = 1 THEN + '', 55'' ELSE '''' END + + CASE WHEN is_table_spool_expensive = 1 THEN + '', 67'' ELSE '''' END + + CASE WHEN is_table_spool_more_rows = 1 THEN + '', 68'' ELSE '''' END + CASE WHEN is_bad_estimate = 1 THEN + '', 56'' ELSE '''' END + - CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END - , 2, 200000) AS opserver_warning , ' + @nl ; + CASE WHEN is_paul_white_electric = 1 THEN '', 57'' ELSE '''' END + + CASE WHEN is_row_goal = 1 THEN '', 58'' ELSE '''' END + + CASE WHEN is_big_spills = 1 THEN '', 59'' ELSE '''' END + + CASE WHEN is_mstvf = 1 THEN '', 60'' ELSE '''' END + + CASE WHEN is_mm_join = 1 THEN '', 61'' ELSE '''' END + + CASE WHEN is_nonsargable = 1 THEN '', 62'' ELSE '''' END + + CASE WHEN CompileTime > 5000 THEN '', 63 '' ELSE '''' END + + CASE WHEN CompileCPU > 5000 THEN '', 64 '' ELSE '''' END + + CASE WHEN CompileMemory > 1024 AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. THEN '', 65 '' ELSE '''' END + + CASE WHEN select_with_writes > 0 THEN '', 66'' ELSE '''' END + , 3, 200000) AS opserver_warning , ' + @nl ; END; - SET @columns += N' ExecutionCount AS [# Executions], - ExecutionsPerMinute AS [Executions / Minute], - PercentExecutions AS [Execution Weight], - SerialDesiredMemory AS [Serial Desired Memory], - SerialRequiredMemory AS [Serial Required Memory], - TotalCPU AS [Total CPU (ms)], - AverageCPU AS [Avg CPU (ms)], - PercentCPU AS [CPU Weight], - TotalDuration AS [Total Duration (ms)], - AverageDuration AS [Avg Duration (ms)], - PercentDuration AS [Duration Weight], - TotalReads AS [Total Reads], - AverageReads AS [Average Reads], - PercentReads AS [Read Weight], - TotalWrites AS [Total Writes], - AverageWrites AS [Average Writes], - PercentWrites AS [Write Weight], - PercentExecutionsByType AS [% Executions (Type)], - PercentCPUByType AS [% CPU (Type)], - PercentDurationByType AS [% Duration (Type)], - PercentReadsByType AS [% Reads (Type)], - PercentWritesByType AS [% Writes (Type)], - TotalReturnedRows AS [Total Rows], - AverageReturnedRows AS [Avg Rows], - MinReturnedRows AS [Min Rows], - MaxReturnedRows AS [Max Rows], - MinGrantKB AS [Minimum Memory Grant KB], - MaxGrantKB AS [Maximum Memory Grant KB], - MinUsedGrantKB AS [Minimum Used Grant KB], - MaxUsedGrantKB AS [Maximum Used Grant KB], - AvgMaxMemoryGrant AS [Average Max Memory Grant], - NumberOfPlans AS [# Plans], - NumberOfDistinctPlans AS [# Distinct Plans], + SET @columns += N' + CONVERT(NVARCHAR(30), CAST((ExecutionCount) AS BIGINT), 1) AS [# Executions], + CONVERT(NVARCHAR(30), CAST((ExecutionsPerMinute) AS BIGINT), 1) AS [Executions / Minute], + CONVERT(NVARCHAR(30), CAST((PercentExecutions) AS BIGINT), 1) AS [Execution Weight], + CONVERT(NVARCHAR(30), CAST((SerialDesiredMemory) AS BIGINT), 1) AS [Serial Desired Memory], + CONVERT(NVARCHAR(30), CAST((SerialRequiredMemory) AS BIGINT), 1) AS [Serial Required Memory], + CONVERT(NVARCHAR(30), CAST((TotalCPU) AS BIGINT), 1) AS [Total CPU (ms)], + CONVERT(NVARCHAR(30), CAST((AverageCPU) AS BIGINT), 1) AS [Avg CPU (ms)], + CONVERT(NVARCHAR(30), CAST((PercentCPU) AS BIGINT), 1) AS [CPU Weight], + CONVERT(NVARCHAR(30), CAST((TotalDuration) AS BIGINT), 1) AS [Total Duration (ms)], + CONVERT(NVARCHAR(30), CAST((AverageDuration) AS BIGINT), 1) AS [Avg Duration (ms)], + CONVERT(NVARCHAR(30), CAST((PercentDuration) AS BIGINT), 1) AS [Duration Weight], + CONVERT(NVARCHAR(30), CAST((TotalReads) AS BIGINT), 1) AS [Total Reads], + CONVERT(NVARCHAR(30), CAST((AverageReads) AS BIGINT), 1) AS [Average Reads], + CONVERT(NVARCHAR(30), CAST((PercentReads) AS BIGINT), 1) AS [Read Weight], + CONVERT(NVARCHAR(30), CAST((TotalWrites) AS BIGINT), 1) AS [Total Writes], + CONVERT(NVARCHAR(30), CAST((AverageWrites) AS BIGINT), 1) AS [Average Writes], + CONVERT(NVARCHAR(30), CAST((PercentWrites) AS BIGINT), 1) AS [Write Weight], + CONVERT(NVARCHAR(30), CAST((PercentExecutionsByType) AS BIGINT), 1) AS [% Executions (Type)], + CONVERT(NVARCHAR(30), CAST((PercentCPUByType) AS BIGINT), 1) AS [% CPU (Type)], + CONVERT(NVARCHAR(30), CAST((PercentDurationByType) AS BIGINT), 1) AS [% Duration (Type)], + CONVERT(NVARCHAR(30), CAST((PercentReadsByType) AS BIGINT), 1) AS [% Reads (Type)], + CONVERT(NVARCHAR(30), CAST((PercentWritesByType) AS BIGINT), 1) AS [% Writes (Type)], + CONVERT(NVARCHAR(30), CAST((TotalReturnedRows) AS BIGINT), 1) AS [Total Rows], + CONVERT(NVARCHAR(30), CAST((AverageReturnedRows) AS BIGINT), 1) AS [Avg Rows], + CONVERT(NVARCHAR(30), CAST((MinReturnedRows) AS BIGINT), 1) AS [Min Rows], + CONVERT(NVARCHAR(30), CAST((MaxReturnedRows) AS BIGINT), 1) AS [Max Rows], + CONVERT(NVARCHAR(30), CAST((MinGrantKB) AS BIGINT), 1) AS [Minimum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxGrantKB) AS BIGINT), 1) AS [Maximum Memory Grant KB], + CONVERT(NVARCHAR(30), CAST((MinUsedGrantKB) AS BIGINT), 1) AS [Minimum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((MaxUsedGrantKB) AS BIGINT), 1) AS [Maximum Used Grant KB], + CONVERT(NVARCHAR(30), CAST((AvgMaxMemoryGrant) AS BIGINT), 1) AS [Average Max Memory Grant], + CONVERT(NVARCHAR(30), CAST((MinSpills) AS BIGINT), 1) AS [Min Spills], + CONVERT(NVARCHAR(30), CAST((MaxSpills) AS BIGINT), 1) AS [Max Spills], + CONVERT(NVARCHAR(30), CAST((TotalSpills) AS BIGINT), 1) AS [Total Spills], + CONVERT(NVARCHAR(30), CAST((AvgSpills) AS MONEY), 1) AS [Avg Spills], + CONVERT(NVARCHAR(30), CAST((NumberOfPlans) AS BIGINT), 1) AS [# Plans], + CONVERT(NVARCHAR(30), CAST((NumberOfDistinctPlans) AS BIGINT), 1) AS [# Distinct Plans], PlanCreationTime AS [Created At], LastExecutionTime AS [Last Execution], - CachedPlanSize AS [Cached Plan Size (KB)], - CompileTime AS [Compile Time (ms)], - CompileCPU AS [Compile CPU (ms)], - CompileMemory AS [Compile memory (KB)], + LastCompletionTime AS [Last Completion], + CONVERT(NVARCHAR(30), CAST((CachedPlanSize) AS BIGINT), 1) AS [Cached Plan Size (KB)], + CONVERT(NVARCHAR(30), CAST((CompileTime) AS BIGINT), 1) AS [Compile Time (ms)], + CONVERT(NVARCHAR(30), CAST((CompileCPU) AS BIGINT), 1) AS [Compile CPU (ms)], + CONVERT(NVARCHAR(30), CAST((CompileMemory) AS BIGINT), 1) AS [Compile memory (KB)], COALESCE(SetOptions, '''') AS [SET Options], PlanHandle AS [Plan Handle], SqlHandle AS [SQL Handle], @@ -4272,40 +5240,44 @@ BEGIN QueryPlanHash AS [Query Plan Hash], StatementStartOffset, StatementEndOffset, + PlanGenerationNum, [Remove Plan Handle From Cache], [Remove SQL Handle From Cache]'; END; - - SET @sql = N' +SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT TOP (@Top) ' + @columns + @nl + N' -FROM ##bou_BlitzCacheProcs +FROM ##BlitzCacheProcs WHERE SPID = @spid ' + @nl; IF @MinimumExecutionCount IS NOT NULL BEGIN - SET @sql += N' AND ExecutionCount >= @minimumExecutionCount ' + @nl; + SET @sql += N' AND ExecutionCount >= @MinimumExecutionCount ' + @nl; END; IF @MinutesBack IS NOT NULL BEGIN - SET @sql += N' AND LastExecutionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; + SET @sql += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) ' + @nl; END; -SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' - WHEN 'reads' THEN N' TotalReads ' - WHEN 'writes' THEN N' TotalWrites ' - WHEN 'duration' THEN N' TotalDuration ' - WHEN 'executions' THEN N' ExecutionCount ' - WHEN 'compiles' THEN N' PlanCreationTime ' - WHEN 'memory grant' THEN N' MaxGrantKB' - WHEN 'avg cpu' THEN N' AverageCPU' - WHEN 'avg reads' THEN N' AverageReads' - WHEN 'avg writes' THEN N' AverageWrites' - WHEN 'avg duration' THEN N' AverageDuration' - WHEN 'avg executions' THEN N' ExecutionsPerMinute' - WHEN 'avg memory grant' THEN N' AvgMaxMemoryGrant' +SELECT @sql += N' ORDER BY ' + CASE @SortOrder WHEN N'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB ' + WHEN N'duplicate' THEN N' plan_multiple_plans ' + WHEN N'spills' THEN N' MaxSpills ' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' END + N' DESC '; SET @sql += N' OPTION (RECOMPILE) ; '; @@ -4322,8 +5294,88 @@ IF @Debug = 1 PRINT SUBSTRING(@sql, 32000, 36000); PRINT SUBSTRING(@sql, 36000, 40000); END; +IF(@OutputType <> 'NONE') +BEGIN + EXEC sp_executesql @sql, N'@Top INT, @spid INT, @MinimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; +END; + +/* + +This section will check if: + * >= 30% of plans were created in the last hour + * Check on the memory_clerks DMV for space used by TokenAndPermUserStore + * Compare that to the size of the buffer pool + * If it's >10%, +*/ +IF EXISTS +( + SELECT 1/0 + FROM #plan_creation AS pc + WHERE pc.percent_1 >= 30 +) +BEGIN + +SELECT @common_version = + CONVERT(DECIMAL(10,2), c.common_version) +FROM #checkversion AS c; + +IF @common_version >= 11 + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' +ELSE + SET @user_perm_sql = N' + SET @buffer_pool_memory_gb = 0; + SELECT @buffer_pool_memory_gb = SUM(single_pages_kb + multi_pages_kb)/ 1024. / 1024. + FROM sys.dm_os_memory_clerks + WHERE type = ''MEMORYCLERK_SQLBUFFERPOOL'';' + +EXEC sys.sp_executesql @user_perm_sql, + N'@buffer_pool_memory_gb DECIMAL(10,2) OUTPUT', + @buffer_pool_memory_gb = @buffer_pool_memory_gb OUTPUT; + +IF @common_version >= 11 +BEGIN + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN (pages_kb / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), (pages_kb / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; +END; + +IF @common_version < 11 +BEGIN + SET @user_perm_sql = N' + SELECT @user_perm_gb = CASE WHEN ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.) >= 2. + THEN CONVERT(DECIMAL(38, 2), ((single_pages_kb + multi_pages_kb) / 1024.0 / 1024.)) + ELSE 0 + END + FROM sys.dm_os_memory_clerks + WHERE type = ''USERSTORE_TOKENPERM'' + AND name = ''TokenAndPermUserStore'';'; +END; + +EXEC sys.sp_executesql @user_perm_sql, + N'@user_perm_gb DECIMAL(10,2) OUTPUT', + @user_perm_gb = @user_perm_gb_out OUTPUT; + +IF @buffer_pool_memory_gb > 0 + BEGIN + IF (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100. >= 10 + BEGIN + SET @is_tokenstore_big = 1; + SET @user_perm_percent = (@user_perm_gb_out / (1. * @buffer_pool_memory_gb)) * 100.; + END + END + +END + -EXEC sp_executesql @sql, N'@Top INT, @spid INT, @minimumExecutionCount INT, @min_back INT', @Top, @@SPID, @MinimumExecutionCount, @MinutesBack; IF @HideSummary = 0 AND @ExportToExcel = 0 BEGIN @@ -4333,723 +5385,763 @@ BEGIN /* Build summary data */ IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE frequent_execution = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 1, 100, 'Execution Pattern', - 'Frequently Executed Queries', - 'http://brentozar.com/blitzcache/frequently-executed-queries/', + 'Frequent Execution', + 'https://www.brentozar.com/blitzcache/frequently-executed-queries/', 'Queries are being executed more than ' + CAST (@execution_threshold AS VARCHAR(5)) + ' times per minute. This can put additional load on the server, even when queries are lightweight.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE parameter_sniffing = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 2, 50, 'Parameterization', 'Parameter Sniffing', - 'http://brentozar.com/blitzcache/parameter-sniffing/', + 'https://www.brentozar.com/blitzcache/parameter-sniffing/', 'There are signs of parameter sniffing (wide variance in rows return or time to execute). Investigate query patterns and tune code appropriately.') ; /* Forced execution plans */ IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE is_forced_plan = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 3, - 5, + 50, 'Parameterization', - 'Forced Plans', - 'http://brentozar.com/blitzcache/forced-plans/', + 'Forced Plan', + 'https://www.brentozar.com/blitzcache/forced-plans/', 'Execution plans have been compiled with forced plans, either through FORCEPLAN, plan guides, or forced parameterization. This will make general tuning efforts less effective.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Cursor', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs + WHERE is_cursor = 1 + AND is_optimistic_cursor = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 4, + 200, + 'Cursors', + 'Optimistic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are optimistic cursors in the plan cache, which can harm performance.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs WHERE is_cursor = 1 + AND is_forward_only_cursor = 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 4, 200, 'Cursors', - 'Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are cursors in the plan cache. This is neither good nor bad, but it is a thing. Cursors are weird in SQL Server.'); + 'Non-forward Only Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'There are non-forward only cursors in the plan cache, which can harm performance.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE is_cursor = 1 - AND is_optimistic_cursor = 1 + AND is_cursor_dynamic = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 4, 200, 'Cursors', - 'Optimistic Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are optimistic cursors in the plan cache, which can harm performance.'); + 'Dynamic Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Dynamic Cursors inhibit parallelism!.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE is_cursor = 1 - AND is_forward_only_cursor = 0 + AND is_fast_forward_cursor = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 4, 200, 'Cursors', - 'Non-forward Only Cursors', - 'http://brentozar.com/blitzcache/cursors-found-slow-queries/', - 'There are non-forward only cursors in the plan cache, which can harm performance.'); + 'Fast Forward Cursors', + 'https://www.brentozar.com/blitzcache/cursors-found-slow-queries/', + 'Fast forward cursors inhibit parallelism!.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE is_forced_parameterized = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 5, 50, 'Parameterization', 'Forced Parameterization', - 'http://brentozar.com/blitzcache/forced-parameterization/', + 'https://www.brentozar.com/blitzcache/forced-parameterization/', 'Execution plans have been compiled with forced parameterization.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_parallel = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 6, 200, 'Execution Plans', - 'Parallelism', - 'http://brentozar.com/blitzcache/parallel-plans-detected/', + 'Parallel', + 'https://www.brentozar.com/blitzcache/parallel-plans-detected/', 'Parallel plans detected. These warrant investigation, but are neither good nor bad.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE near_parallel = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 7, 200, 'Execution Plans', 'Nearly Parallel', - 'http://brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', + 'https://www.brentozar.com/blitzcache/query-cost-near-cost-threshold-parallelism/', 'Queries near the cost threshold for parallelism. These may go parallel when you least expect it.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE plan_warnings = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 8, 50, 'Execution Plans', - 'Query Plan Warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', + 'Plan Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', 'Warnings detected in execution plans. SQL Server is telling you that something bad is going on that requires your attention.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE long_running = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 9, 50, 'Performance', - 'Long Running Queries', - 'http://brentozar.com/blitzcache/long-running-queries/', + 'Long Running Query', + 'https://www.brentozar.com/blitzcache/long-running-queries/', 'Long running queries have been found. These are queries with an average duration longer than ' + CAST(@long_running_query_warning_seconds / 1000 / 1000 AS VARCHAR(5)) + ' second(s). These queries should be investigated for additional tuning options.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.missing_index_count > 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 10, 50, 'Performance', - 'Missing Index Request', - 'http://brentozar.com/blitzcache/missing-index-request/', + 'Missing Indexes', + 'https://www.brentozar.com/blitzcache/missing-index-request/', 'Queries found with missing indexes.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.downlevel_estimator = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 13, 200, 'Cardinality', - 'Legacy Cardinality Estimator in Use', - 'http://brentozar.com/blitzcache/legacy-cardinality-estimator/', + 'Downlevel CE', + 'https://www.brentozar.com/blitzcache/legacy-cardinality-estimator/', 'A legacy cardinality estimator is being used by one or more queries. Investigate whether you need to be using this cardinality estimator. This may be caused by compatibility levels, global trace flags, or query level trace flags.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE implicit_conversions = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 14, 50, 'Performance', 'Implicit Conversions', - 'http://brentozar.com/go/implicit', + 'https://www.brentozar.com/go/implicit', 'One or more queries are comparing two fields that are not of the same data type.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE busy_loops = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 16, - 10, + 100, 'Performance', - 'Frequently executed operators', - 'http://brentozar.com/blitzcache/busy-loops/', + 'Busy Loops', + 'https://www.brentozar.com/blitzcache/busy-loops/', 'Operations have been found that are executed 100 times more often than the number of rows returned by each iteration. This is an indicator that something is off in query execution.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE tvf_join = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 17, 50, 'Performance', - 'Joining to table valued functions', - 'http://brentozar.com/blitzcache/tvf-join/', + 'Function Join', + 'https://www.brentozar.com/blitzcache/tvf-join/', 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE compile_timeout = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 18, 50, 'Execution Plans', - 'Compilation timeout', - 'http://brentozar.com/blitzcache/compilation-timeout/', + 'Compilation Timeout', + 'https://www.brentozar.com/blitzcache/compilation-timeout/', 'Query compilation timed out for one or more queries. SQL Server did not find a plan that meets acceptable performance criteria in the time allotted so the best guess was returned. There is a very good chance that this plan isn''t even below average - it''s probably terrible.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE compile_memory_limit_exceeded = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 19, 50, 'Execution Plans', - 'Compilation memory limit exceeded', - 'http://brentozar.com/blitzcache/compile-memory-limit-exceeded/', + 'Compile Memory Limit Exceeded', + 'https://www.brentozar.com/blitzcache/compile-memory-limit-exceeded/', 'The optimizer has a limited amount of memory available. One or more queries are complex enough that SQL Server was unable to allocate enough memory to fully optimize the query. A best fit plan was found, and it''s probably terrible.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE warning_no_join_predicate = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 20, - 10, + 50, 'Execution Plans', - 'No join predicate', - 'http://brentozar.com/blitzcache/no-join-predicate/', + 'No Join Predicate', + 'https://www.brentozar.com/blitzcache/no-join-predicate/', 'Operators in a query have no join predicate. This means that all rows from one table will be matched with all rows from anther table producing a Cartesian product. That''s a whole lot of rows. This may be your goal, but it''s important to investigate why this is happening.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs - WHERE plan_multiple_plans = 1 + FROM ##BlitzCacheProcs + WHERE plan_multiple_plans > 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 21, 200, 'Execution Plans', - 'Multiple execution plans', - 'http://brentozar.com/blitzcache/multiple-plans/', + 'Multiple Plans', + 'https://www.brentozar.com/blitzcache/multiple-plans/', 'Queries exist with multiple execution plans (as determined by query_plan_hash). Investigate possible ways to parameterize these queries or otherwise reduce the plan count.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE unmatched_index_count > 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 22, 100, 'Performance', - 'Unmatched indexes', - 'http://brentozar.com/blitzcache/unmatched-indexes', + 'Unmatched Indexes', + 'https://www.brentozar.com/blitzcache/unmatched-indexes', 'An index could have been used, but SQL Server chose not to use it - likely due to parameterization and filtered indexes.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE unparameterized_query = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 23, 100, 'Parameterization', - 'Unparameterized queries', - 'http://brentozar.com/blitzcache/unparameterized-queries', + 'Unparameterized Query', + 'https://www.brentozar.com/blitzcache/unparameterized-queries', 'Unparameterized queries found. These could be ad hoc queries, data exploration, or queries using "OPTIMIZE FOR UNKNOWN".'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs + FROM ##BlitzCacheProcs WHERE is_trivial = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 24, 100, 'Execution Plans', 'Trivial Plans', - 'http://brentozar.com/blitzcache/trivial-plans', + 'https://www.brentozar.com/blitzcache/trivial-plans', 'Trivial plans get almost no optimization. If you''re finding these in the top worst queries, something may be going wrong.'); IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_forced_serial= 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 25, 10, 'Execution Plans', 'Forced Serialization', - 'http://www.brentozar.com/blitzcache/forced-serialization/', + 'https://www.brentozar.com/blitzcache/forced-serialization/', 'Something in your plan is forcing a serial query. Further investigation is needed if this is not by design.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_key_lookup_expensive= 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 26, 100, 'Execution Plans', - 'Expensive Key Lookups', - 'http://www.brentozar.com/blitzcache/expensive-key-lookups/', + 'Expensive Key Lookup', + 'https://www.brentozar.com/blitzcache/expensive-key-lookups/', 'There''s a key lookup in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_remote_query_expensive= 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 28, 100, 'Execution Plans', 'Expensive Remote Query', - 'http://www.brentozar.com/blitzcache/expensive-remote-query/', + 'https://www.brentozar.com/blitzcache/expensive-remote-query/', 'There''s a remote query in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.trace_flags_session IS NOT NULL AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 29, - 100, + 200, 'Trace Flags', 'Session Level Trace Flags Enabled', 'https://www.brentozar.com/blitz/trace-flags-enabled-globally/', 'Someone is enabling session level Trace Flags in a query.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_unused_grant IS NOT NULL AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 30, 100, - 'Unused memory grants', - 'Queries are asking for more memory than they''re using', + 'Memory Grant', + 'Unused Memory Grant', 'https://www.brentozar.com/blitzcache/unused-memory-grants/', 'Queries have large unused memory grants. This can cause concurrency issues, if queries are waiting a long time to get memory to run.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.function_count > 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 31, 100, 'Compute Scalar That References A Function', - 'This could be trouble if you''re using Scalar Functions or MSTVFs', + 'Calls Functions', 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', 'Both of these will force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.clr_function_count > 0 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 32, 100, 'Compute Scalar That References A CLR Function', - 'This could be trouble if your CLR functions perform data access', + 'Calls CLR Functions', 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', - 'May force queries to run serially, run at least once per row, and may result in poor cardinlity estimates.') ; + 'May force queries to run serially, run at least once per row, and may result in poor cardinality estimates.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_table_variable = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 33, 100, 'Table Variables detected', - 'Beware nasty side effects', + 'Table Variables', 'https://www.brentozar.com/blitzcache/table-variables/', 'All modifications are single threaded, and selects have really low row estimates.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.no_stats_warning = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 35, 100, - 'Columns with no statistics', - 'Poor cardinality estimates may ensue', + 'Statistics', + 'Columns With No Statistics', 'https://www.brentozar.com/blitzcache/columns-no-statistics/', 'Sometimes this happens with indexed views, other times because auto create stats is turned off.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.relop_warnings = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 36, 100, - 'Operator Warnings', - 'SQL is throwing operator level plan warnings', - 'http://brentozar.com/blitzcache/query-plan-warnings/', + 'Warnings', + 'Operator Warnings', + 'https://www.brentozar.com/blitzcache/query-plan-warnings/', 'Check the plan for more details.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_table_scan = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 37, 100, - 'Table Scans', - 'Your database has HEAPs', + 'Indexes', + 'Table Scans (Heaps)', 'https://www.brentozar.com/archive/2012/05/video-heaps/', 'This may not be a problem. Run sp_BlitzIndex for more information.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.backwards_scan = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 38, - 100, + 200, + 'Indexes', 'Backwards Scans', - 'Indexes are being read backwards', 'https://www.brentozar.com/blitzcache/backwards-scans/', 'This isn''t always a problem. They can cause serial zones in plans, and may need an index to match sort order.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.forced_index = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 39, 100, - 'Index forcing', - 'Someone is using hints to force index usage', + 'Indexes', + 'Forced Indexes', 'https://www.brentozar.com/blitzcache/optimizer-forcing/', 'This can cause inefficient plans, and will prevent missing index requests.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.forced_seek = 1 - OR p.forced_scan = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 40, + 100, + 'Indexes', + 'Forced Seeks', + 'https://www.brentozar.com/blitzcache/optimizer-forcing/', + 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.forced_scan = 1 + AND SPID = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 40, 100, - 'Seek/Scan forcing', - 'Someone is using hints to force index seeks/scans', + 'Indexes', + 'Forced Scans', 'https://www.brentozar.com/blitzcache/optimizer-forcing/', 'This can cause inefficient plans by taking seek vs scan choice away from the optimizer.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.columnstore_row_mode = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 41, 100, - 'ColumnStore indexes operating in Row Mode', - 'Batch Mode is optimal for ColumnStore indexes', + 'Indexes', + 'ColumnStore Row Mode', 'https://www.brentozar.com/blitzcache/columnstore-indexes-operating-row-mode/', 'ColumnStore indexes operating in Row Mode indicate really poor query choices.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_computed_scalar = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 42, 50, - 'Computed Columns Referencing Scalar UDFs', - 'This makes a whole lot of stuff run serially', + 'Functions', + 'Computed Column UDF', 'https://www.brentozar.com/blitzcache/computed-columns-referencing-functions/', 'This can cause a whole mess of bad serializartion problems.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_sort_expensive = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 43, 100, 'Execution Plans', 'Expensive Sort', - 'http://www.brentozar.com/blitzcache/expensive-sorts/', + 'https://www.brentozar.com/blitzcache/expensive-sorts/', 'There''s a sort in your plan that costs >=50% of the total plan cost.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_computed_filter = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 44, 50, - 'Filters Referencing Scalar UDFs', - 'This forces serialization', + 'Functions', + 'Filter UDF', 'https://www.brentozar.com/blitzcache/compute-scalar-functions/', 'Someone put a Scalar UDF in the WHERE clause!') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.index_ops >= 5 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 45, 100, - 'Many Indexes Modified', - 'Write Queries Are Hitting >= 5 Indexes', + 'Indexes', + '>= 5 Indexes Modified', 'https://www.brentozar.com/blitzcache/many-indexes-modified/', 'This can cause lots of hidden I/O -- Run sp_BlitzIndex for more information.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_row_level = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 46, - 100, - 'Plan Confusion', - 'Row Level Security is in use', + 200, + 'Complexity', + 'Row Level Security', 'https://www.brentozar.com/blitzcache/row-level-security/', 'You may see a lot of confusing junk in your query plan.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_spatial = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 47, 200, - 'Spatial Abuse', - 'You hit a Spatial Index', + 'Complexity', + 'Spatial Index', 'https://www.brentozar.com/blitzcache/spatial-indexes/', 'Purely informational.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.index_dml = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 48, 150, + 'Complexity', 'Index DML', - 'Indexes were created or dropped', 'https://www.brentozar.com/blitzcache/index-dml/', 'This can cause recompiles and stuff.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.table_dml = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 49, 150, - 'Table DML', - 'Tables were created or dropped', + 'Complexity', + 'Table DML', 'https://www.brentozar.com/blitzcache/table-dml/', 'This can cause recompiles and stuff.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.long_running_low_cpu = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 50, 150, + 'Blocking', 'Long Running Low CPU', - 'You have a query that runs for much longer than it uses CPU', 'https://www.brentozar.com/blitzcache/long-running-low-cpu/', 'This can be a sign of blocking, linked servers, or poor client application code (ASYNC_NETWORK_IO).') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.low_cost_high_cpu = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 51, 150, + 'Complexity', 'Low Cost Query With High CPU', - 'You have a low cost query that uses a lot of CPU', 'https://www.brentozar.com/blitzcache/low-cost-high-cpu/', 'This can be a sign of functions or Dynamic SQL that calls black-box code.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.stale_stats = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 52, 150, - 'Biblical Statistics', - 'Statistics used in queries are >7 days old with >100k modifications', + 'Statistics', + 'Statistics used have > 100k modifications in the last 7 days', 'https://www.brentozar.com/blitzcache/stale-statistics/', 'Ever heard of updating statistics?') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_adaptive = 1 AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 53, - 150, - 'Adaptive joins', - 'This is pretty cool -- you''re living in the future.', + 200, + 'Complexity', + 'Adaptive joins', 'https://www.brentozar.com/blitzcache/adaptive-joins/', - 'Joe Sack rules.') ; + 'This join will sometimes do seeks, and sometimes do scans.') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_spool_expensive = 1 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 54, 150, + 'Indexes', 'Expensive Index Spool', - 'You have an index spool, this is usually a sign that there''s an index missing somewhere.', 'https://www.brentozar.com/blitzcache/eager-index-spools/', 'Check operator predicates and output for index definition guidance') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_spool_more_rows = 1 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 55, 150, - 'Index Spools Many Rows', - 'You have an index spool that spools more rows than the query returns', + 'Indexes', + 'Large Index Row Spool', 'https://www.brentozar.com/blitzcache/eager-index-spools/', 'Check operator predicates and output for index definition guidance') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_bad_estimate = 1 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 56, 100, - 'Potentially bad cardinality estimates', - 'Estimated rows are different from average rows by a factor of 10000', + 'Complexity', + 'Row Estimate Mismatch', 'https://www.brentozar.com/blitzcache/bad-estimates/', - 'This may indicate a performance problem if mismatches occur regularly') ; + 'Estimated rows are different from average rows by a factor of 10000. This may indicate a performance problem if mismatches occur regularly') ; IF EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheProcs p + FROM ##BlitzCacheProcs p WHERE p.is_paul_white_electric = 1 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, - 998, + 57, 200, 'Is Paul White Electric?', 'This query has a Switch operator in it!', - 'http://sqlblog.com/blogs/paul_white/archive/2013/06/11/hello-operator-my-switch-is-bored.aspx', + 'https://www.sql.kiwi/2013/06/hello-operator-my-switch-is-bored.html', 'You should email this query plan to Paul: SQLkiwi at gmail dot com') ; - IF @v >= 14 + IF @v >= 14 OR (@v = 13 AND @build >= 5026) BEGIN - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) SELECT @@SPID, - 999, + 997, 200, 'Database Level Statistics', 'The database ' + sa.[Database] + ' last had a stats update on ' + CONVERT(NVARCHAR(10), CONVERT(DATE, MAX(sa.LastUpdate))) + ' and has ' + CONVERT(NVARCHAR(10), AVG(sa.ModificationCount)) + ' modifications on average.' AS [Finding], @@ -5060,19 +6152,164 @@ BEGIN HAVING MAX(sa.LastUpdate) <= DATEADD(DAY, -7, SYSDATETIME()) AND AVG(sa.ModificationCount) >= 100000; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_row_goal = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 58, + 200, + 'Complexity', + 'Row Goals', + 'https://www.brentozar.com/go/rowgoals/', + 'This query had row goals introduced, which can be good or bad, and should be investigated for high read queries.') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_big_spills = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 59, + 100, + 'TempDB', + '>500mb Spills', + 'https://www.brentozar.com/blitzcache/tempdb-spills/', + 'This query spills >500mb to tempdb on average. One way or another, this query didn''t get enough memory') ; + END; + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_mstvf = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 60, + 100, + 'Functions', + 'MSTVFs', + 'https://www.brentozar.com/blitzcache/tvf-join/', + 'Execution plans have been found that join to table valued functions (TVFs). TVFs produce inaccurate estimates of the number of rows returned and can lead to any number of query plan problems.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_mm_join = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 61, + 100, + 'Complexity', + 'Many to Many Merge', + 'https://www.brentozar.com/archive/2018/04/many-mysteries-merge-joins/', + 'These use secret worktables that could be doing lots of reads. Occurs when join inputs aren''t known to be unique. Can be really bad when parallel.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_nonsargable = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 62, + 50, + 'Non-SARGable queries', + 'non-SARGables', + 'https://www.brentozar.com/blitzcache/non-sargable-predicates/', + 'Looks for intrinsic functions and expressions as predicates, and leading wildcard LIKE searches.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileTime > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 63, + 100, + 'Complexity', + 'Long Compile Time', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries are taking >5 seconds to compile. This can be normal for large plans, but be careful if they compile frequently'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileCPU > 5000 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 64, + 50, + 'Complexity', + 'High Compile CPU', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking >5 seconds of CPU to compile. If CPU is high and plans like this compile frequently, they may be related'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE CompileMemory > 1024 + AND ((CompileMemory) / (1 * CASE WHEN MaxCompileMemory = 0 THEN 1 ELSE MaxCompileMemory END) * 100.) >= 10. + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 65, + 50, + 'Complexity', + 'High Compile Memory', + 'https://www.brentozar.com/blitzcache/high-compilers/', + 'Queries taking 10% of Max Compile Memory. If you see high RESOURCE_SEMAPHORE_QUERY_COMPILE waits, these may be related'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.select_with_writes = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 66, + 50, + 'Complexity', + 'Selects w/ Writes', + 'https://dba.stackexchange.com/questions/191825/', + 'This is thrown when reads cause writes that are not already flagged as big spills (2016+) or index spools.'); + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_expensive = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 67, + 150, + 'Expensive Table Spool', + 'You have a table spool, this is usually a sign that queries are doing unnecessary work', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join') ; + + IF EXISTS (SELECT 1/0 + FROM ##BlitzCacheProcs p + WHERE p.is_table_spool_more_rows = 1 + ) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + VALUES (@@SPID, + 68, + 150, + 'Table Spools Many Rows', + 'You have a table spool that spools more rows than the query returns', + 'https://sqlperformance.com/2019/09/sql-performance/nested-loops-joins-performance-spools', + 'Check for non-SARGable predicates, or a lot of work being done inside a nested loops join'); IF EXISTS (SELECT 1/0 FROM #plan_creation p WHERE (p.percent_24 > 0) AND SPID = @@SPID) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) SELECT SPID, 999, - 254, + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 1 ELSE 254 END AS Priority, 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_24, 0) > 75 THEN 'Plan Cache Instability' ELSE 'Plan Cache Stability' END AS Finding, + 'https://www.brentozar.com/archive/2018/07/tsql2sday-how-much-plan-cache-history-do-you-have/', 'You have ' + CONVERT(NVARCHAR(10), ISNULL(p.total_plans, 0)) + ' total plans in your cache, with ' + CONVERT(NVARCHAR(10), ISNULL(p.percent_24, 0)) @@ -5080,18 +6317,67 @@ BEGIN + CONVERT(NVARCHAR(10), ISNULL(p.percent_4, 0)) + '% created in the past 4 hours, and ' + CONVERT(NVARCHAR(10), ISNULL(p.percent_1, 0)) - + '% created in the past 1 hour.', - '', - 'If these percentages are high, it may be a sign of memory pressure or plan cache instability.' + + '% created in the past 1 hour. ' + + 'When these percentages are high, it may be a sign of memory pressure or plan cache instability.' FROM #plan_creation p ; - + + IF EXISTS (SELECT 1/0 + FROM #plan_usage p + WHERE p.percent_duplicate > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_duplicate, 0) > 75 THEN 'Many Duplicate Plans' ELSE 'Duplicate Plans' END AS Finding, + 'https://www.brentozar.com/archive/2018/03/why-multiple-plans-for-one-query-are-bad/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_duplicate) + + '% are duplicates with more than 5 entries' + + ', meaning similar queries are generating the same plan repeatedly.' + + ' Forced Parameterization may fix the issue. To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; + + IF EXISTS (SELECT 1/0 + FROM #plan_usage p + WHERE p.percent_single > 5 + AND spid = @@SPID) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT spid, + 999, + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 1 ELSE 254 END AS Priority, + 'Plan Cache Information', + CASE WHEN ISNULL(p.percent_single, 0) > 75 THEN 'Many Single-Use Plans' ELSE 'Single-Use Plans' END AS Finding, + 'https://www.brentozar.com/blitz/single-use-plans-procedure-cache/', + 'You have ' + CONVERT(NVARCHAR(10), p.total_plans) + + ' plans in your cache, and ' + + CONVERT(NVARCHAR(10), p.percent_single) + + '% are single use plans' + + ', meaning SQL Server thinks it''s seeing a lot of "new" queries and creating plans for them.' + + ' Forced Parameterization and/or Optimize For Ad Hoc Workloads may fix the issue.' + + 'To find troublemakers, use: EXEC sp_BlitzCache @SortOrder = ''query hash''; ' + FROM #plan_usage AS p ; + + IF @is_tokenstore_big = 1 + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT @@SPID, + 69, + 10, + N'Large USERSTORE_TOKENPERM cache: ' + CONVERT(NVARCHAR(11), @user_perm_gb_out) + N'GB', + N'The USERSTORE_TOKENPERM is taking up ' + CONVERT(NVARCHAR(11), @user_perm_percent) + + N'% of the buffer pool, and your plan cache seems to be unstable', + N'https://www.brentozar.com/go/userstore', + N'A growing USERSTORE_TOKENPERM cache can cause the plan cache to clear out' + IF @v >= 11 BEGIN IF EXISTS (SELECT 1/0 FROM #trace_flags AS tf WHERE tf.global_trace_flags IS NOT NULL ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 1000, 255, @@ -5102,10 +6388,10 @@ BEGIN END; IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr + FROM ##BlitzCacheResults AS bcr WHERE bcr.Priority = 2147483646 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 2147483646, 255, @@ -5117,10 +6403,10 @@ BEGIN IF NOT EXISTS (SELECT 1/0 - FROM ##bou_BlitzCacheResults AS bcr + FROM ##BlitzCacheResults AS bcr WHERE bcr.Priority = 2147483647 ) - INSERT INTO ##bou_BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) + INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 2147483647, 255, @@ -5138,7 +6424,7 @@ BEGIN URL, Details, CheckID - FROM ##bou_BlitzCacheResults + FROM ##BlitzCacheResults WHERE SPID = @@SPID GROUP BY Priority, FindingsGroup, @@ -5146,30 +6432,30 @@ BEGIN URL, Details, CheckID - ORDER BY Priority ASC, CheckID ASC + ORDER BY Priority ASC, FindingsGroup, Finding, CheckID ASC OPTION (RECOMPILE); END; IF @Debug = 1 BEGIN - SELECT '##bou_BlitzCacheResults' AS table_name, * - FROM ##bou_BlitzCacheResults + SELECT '##BlitzCacheResults' AS table_name, * + FROM ##BlitzCacheResults OPTION ( RECOMPILE ); - SELECT '##bou_BlitzCacheProcs' AS table_name, * - FROM ##bou_BlitzCacheProcs + SELECT '##BlitzCacheProcs' AS table_name, * + FROM ##BlitzCacheProcs OPTION ( RECOMPILE ); - SELECT '#statements' AS table_name, * + SELECT '#statements' AS table_name, * FROM #statements AS s OPTION (RECOMPILE); - SELECT '#query_plan' AS table_name, * + SELECT '#query_plan' AS table_name, * FROM #query_plan AS qp OPTION (RECOMPILE); - SELECT '#relop' AS table_name, * + SELECT '#relop' AS table_name, * FROM #relop AS r OPTION (RECOMPILE); @@ -5212,7 +6498,27 @@ IF @Debug = 1 SELECT '#variable_info' AS table_name, * FROM #variable_info AS vi OPTION ( RECOMPILE ); - + + SELECT '#missing_index_xml' AS table_name, * + FROM #missing_index_xml AS mix + OPTION ( RECOMPILE ); + + SELECT '#missing_index_schema' AS table_name, * + FROM #missing_index_schema AS mis + OPTION ( RECOMPILE ); + + SELECT '#missing_index_usage' AS table_name, * + FROM #missing_index_usage AS miu + OPTION ( RECOMPILE ); + + SELECT '#missing_index_detail' AS table_name, * + FROM #missing_index_detail AS mid + OPTION ( RECOMPILE ); + + SELECT '#missing_index_pretty' AS table_name, * + FROM #missing_index_pretty AS mip + OPTION ( RECOMPILE ); + SELECT '#plan_creation' AS table_name, * FROM #plan_creation OPTION ( RECOMPILE ); @@ -5233,9 +6539,15 @@ IF @Debug = 1 FROM #trace_flags OPTION ( RECOMPILE ); - END; + SELECT '#plan_usage' AS table_name, * + FROM #plan_usage + OPTION ( RECOMPILE ); + END; + IF @OutputTableName IS NOT NULL + --Allow for output to ##DB so don't check for DB or schema name here + GOTO OutputResultsToTable; RETURN; --Avoid going into the AllSort GOTO /*Begin code to sort by all*/ @@ -5245,6 +6557,7 @@ RAISERROR('Beginning all sort loop', 0, 1) WITH NOWAIT; IF ( @Top > 10 + AND @SkipAnalysis = 0 AND @BringThePain = 0 ) BEGIN @@ -5287,67 +6600,55 @@ IF OBJECT_ID('tempdb.. #bou_allsort') IS NULL CREATE TABLE #bou_allsort ( Id INT IDENTITY(1, 1), - DatabaseName VARCHAR(128), + DatabaseName NVARCHAR(128), Cost FLOAT, QueryText NVARCHAR(MAX), - QueryType NVARCHAR(256), + QueryType NVARCHAR(258), Warnings VARCHAR(MAX), QueryPlan XML, missing_indexes XML, implicit_conversion_info XML, cached_execution_parameters XML, - ExecutionCount BIGINT, + ExecutionCount NVARCHAR(30), ExecutionsPerMinute MONEY, ExecutionWeight MONEY, - TotalCPU BIGINT, - AverageCPU BIGINT, + TotalCPU NVARCHAR(30), + AverageCPU NVARCHAR(30), CPUWeight MONEY, - TotalDuration BIGINT, - AverageDuration BIGINT, + TotalDuration NVARCHAR(30), + AverageDuration NVARCHAR(30), DurationWeight MONEY, - TotalReads BIGINT, - AverageReads BIGINT, + TotalReads NVARCHAR(30), + AverageReads NVARCHAR(30), ReadWeight MONEY, - TotalWrites BIGINT, - AverageWrites BIGINT, + TotalWrites NVARCHAR(30), + AverageWrites NVARCHAR(30), WriteWeight MONEY, AverageReturnedRows MONEY, - MinGrantKB BIGINT, - MaxGrantKB BIGINT, - MinUsedGrantKB BIGINT, - MaxUsedGrantKB BIGINT, + MinGrantKB NVARCHAR(30), + MaxGrantKB NVARCHAR(30), + MinUsedGrantKB NVARCHAR(30), + MaxUsedGrantKB NVARCHAR(30), AvgMaxMemoryGrant MONEY, + MinSpills NVARCHAR(30), + MaxSpills NVARCHAR(30), + TotalSpills NVARCHAR(30), + AvgSpills MONEY, PlanCreationTime DATETIME, LastExecutionTime DATETIME, + LastCompletionTime DATETIME, PlanHandle VARBINARY(64), SqlHandle VARBINARY(64), SetOptions VARCHAR(MAX), + QueryHash BINARY(8), + PlanGenerationNum NVARCHAR(30), + RemovePlanHandleFromCache NVARCHAR(200), Pattern NVARCHAR(20) ); END; -DECLARE @AllSortSql NVARCHAR(MAX) = N''; -DECLARE @MemGrant BIT; -SELECT @MemGrant = CASE WHEN ( - ( @v < 11 ) - OR ( - @v = 11 - AND @build < 6020 - ) - OR ( - @v = 12 - AND @build < 5000 - ) - OR ( - @v = 13 - AND @build < 1601 - ) - ) THEN 0 - ELSE 1 - END; - -IF LOWER(@SortOrder) = 'all' +IF @SortOrder = 'all' BEGIN RAISERROR('Beginning for ALL', 0, 1) WITH NOWAIT; SET @AllSortSql += N' @@ -5356,59 +6657,64 @@ SET @AllSortSql += N' INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); '; - IF @MemGrant = 0 + IF @VersionShowsMemoryGrants = 0 BEGIN IF @ExportToExcel = 1 BEGIN @@ -5420,29 +6726,24 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - IF @MemGrant = 1 + IF @VersionShowsMemoryGrants = 1 BEGIN SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; IF @ExportToExcel = 1 @@ -5455,23 +6756,76 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; END; -IF LOWER(@SortOrder) = 'all avg' +IF @SortOrder = 'all avg' BEGIN RAISERROR('Beginning for ALL AVG', 0, 1) WITH NOWAIT; SET @AllSortSql += N' @@ -5480,59 +6834,64 @@ SET @AllSortSql += N' INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg cpu'', + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg cpu'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg reads'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg reads'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg writes'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg writes'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg duration'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg duration'' WHERE Pattern IS NULL OPTION(RECOMPILE); SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg executions'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg executions'' WHERE Pattern IS NULL OPTION(RECOMPILE); '; - IF @MemGrant = 0 + IF @VersionShowsMemoryGrants = 0 BEGIN IF @ExportToExcel = 1 BEGIN @@ -5544,29 +6903,24 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; - IF @MemGrant = 1 + IF @VersionShowsMemoryGrants = 1 BEGIN SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); - INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) - EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''memory grant'', @IgnoreSqlHandles = @ISH, @DatabaseName = @i_DatabaseName WITH RECOMPILE; + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg memory grant'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; UPDATE #bou_allsort SET Pattern = ''avg memory grant'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; IF @ExportToExcel = 1 @@ -5579,18 +6933,72 @@ SET @AllSortSql += N' missing_indexes = NULL OPTION (RECOMPILE); - UPDATE ##bou_BlitzCacheProcs + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 0 + BEGIN + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs + SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) + OPTION(RECOMPILE);'; + END; + + END; + + IF @VersionShowsSpills = 1 + BEGIN + SET @AllSortSql += N' SELECT TOP 1 @ISH = STUFF((SELECT DISTINCT N'','' + CONVERT(NVARCHAR(MAX),b2.SqlHandle, 1) FROM #bou_allsort AS b2 FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''NVARCHAR(MAX)''), 1, 1, N'''') OPTION(RECOMPILE); + + INSERT #bou_allsort ( DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters, ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache ) + + EXEC sp_BlitzCache @ExpertMode = 0, @HideSummary = 1, @Top = @i_Top, @SortOrder = ''avg spills'', @IgnoreSqlHandles = @ISH, + @DatabaseName = @i_DatabaseName, @SkipAnalysis = @i_SkipAnalysis, @OutputDatabaseName = @i_OutputDatabaseName, @OutputSchemaName = @i_OutputSchemaName, @OutputTableName = @i_OutputTableName, @CheckDateOverride = @i_CheckDateOverride, @MinutesBack = @i_MinutesBack WITH RECOMPILE; + + UPDATE #bou_allsort SET Pattern = ''avg spills'' WHERE Pattern IS NULL OPTION(RECOMPILE);'; + IF @ExportToExcel = 1 + BEGIN + SET @AllSortSql += N' UPDATE #bou_allsort + SET + QueryPlan = NULL, + implicit_conversion_info = NULL, + cached_execution_parameters = NULL, + missing_indexes = NULL + OPTION (RECOMPILE); + + UPDATE ##BlitzCacheProcs SET QueryText = SUBSTRING(REPLACE(REPLACE(REPLACE(LTRIM(RTRIM(QueryText)),'' '',''<>''),''><'',''''),''<>'','' ''), 1, 32000) OPTION(RECOMPILE);'; END; - SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, - TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, - ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, - MaxUsedGrantKB, AvgMaxMemoryGrant, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions - FROM #bou_allsort - ORDER BY Id - OPTION(RECOMPILE); '; + END; + + IF(@OutputType <> 'NONE') + BEGIN + SET @AllSortSql += N' SELECT DatabaseName, Cost, QueryText, QueryType, Warnings, QueryPlan, missing_indexes, implicit_conversion_info, cached_execution_parameters,ExecutionCount, ExecutionsPerMinute, ExecutionWeight, + TotalCPU, AverageCPU, CPUWeight, TotalDuration, AverageDuration, DurationWeight, TotalReads, AverageReads, + ReadWeight, TotalWrites, AverageWrites, WriteWeight, AverageReturnedRows, MinGrantKB, MaxGrantKB, MinUsedGrantKB, + MaxUsedGrantKB, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, SetOptions, QueryHash, PlanGenerationNum, RemovePlanHandleFromCache, Pattern + FROM #bou_allsort + ORDER BY Id + OPTION(RECOMPILE); '; + END; END; IF @Debug = 1 @@ -5607,11 +7015,606 @@ END; PRINT SUBSTRING(@AllSortSql, 36000, 40000); END; - EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT', @i_DatabaseName = @DatabaseName, @i_Top = @Top; + EXEC sys.sp_executesql @stmt = @AllSortSql, @params = N'@i_DatabaseName NVARCHAR(128), @i_Top INT, @i_SkipAnalysis BIT, @i_OutputDatabaseName NVARCHAR(258), @i_OutputSchemaName NVARCHAR(258), @i_OutputTableName NVARCHAR(258), @i_CheckDateOverride DATETIMEOFFSET, @i_MinutesBack INT ', + @i_DatabaseName = @DatabaseName, @i_Top = @Top, @i_SkipAnalysis = @SkipAnalysis, @i_OutputDatabaseName = @OutputDatabaseName, @i_OutputSchemaName = @OutputSchemaName, @i_OutputTableName = @OutputTableName, @i_CheckDateOverride = @CheckDateOverride, @i_MinutesBack = @MinutesBack; +/* Avoid going into OutputResultsToTable + ... otherwise the last result (e.g. spills) would be recorded twice into the output table. +*/ +RETURN; /*End of AllSort section*/ + +/*Begin code to write results to table */ +OutputResultsToTable: + +RAISERROR('Writing results to table.', 0, 1) WITH NOWAIT; + +SELECT @OutputServerName = QUOTENAME(@OutputServerName), + @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), + @OutputSchemaName = QUOTENAME(@OutputSchemaName), + @OutputTableName = QUOTENAME(@OutputTableName); + +/* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ +DECLARE @ValidOutputServer BIT; +DECLARE @ValidOutputLocation BIT; +DECLARE @LinkedServerDBCheck NVARCHAR(2000); +DECLARE @ValidLinkedServerDB INT; +DECLARE @tmpdbchk table (cnt int); +IF @OutputServerName IS NOT NULL + BEGIN + + IF @Debug IN (1, 2) RAISERROR('Outputting to a remote server.', 0, 1) WITH NOWAIT; + + IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; +ELSE + BEGIN + IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @StringToExecute NVARCHAR(MAX) = N'' ; + + IF @ValidOutputLocation = 1 + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + CONVERT + ( + nvarchar(MAX), + N'(ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,N'[',N''),N']',N'') + N'] PRIMARY KEY CLUSTERED(ID ASC));' + ); + + SET @StringToExecute += N'IF EXISTS(SELECT * FROM ' + +@OutputDatabaseName + +N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + +@OutputSchemaName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + +@OutputSchemaName + +N''' AND QUOTENAME(TABLE_NAME) = ''' + +@OutputTableName + +N''') AND EXISTS (SELECT * FROM ' + +@OutputDatabaseName+ + N'.sys.computed_columns WHERE [name] = N''PlanCreationTimeHours'' AND QUOTENAME(OBJECT_NAME(object_id)) = N''' + +@OutputTableName + +N''' AND [definition] = N''(datediff(hour,[PlanCreationTime],sysdatetime()))'') +BEGIN + RAISERROR(''We noticed that you are running an old computed column definition for PlanCreationTimeHours, fixing that now'',0,0) WITH NOWAIT; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' DROP COLUMN [PlanCreationTimeHours]; + ALTER TABLE '+@OutputDatabaseName+N'.'+@OutputSchemaName+N'.'+@OutputTableName+N' ADD [PlanCreationTimeHours] AS DATEDIFF(HOUR,CONVERT(DATETIMEOFFSET(7),[PlanCreationTime]),[CheckDate]); +END '; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputSchemaName+'''',''''''+@OutputSchemaName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,''''+@OutputTableName+'''',''''''+@OutputTableName+''''''); + SET @StringToExecute = REPLACE(@StringToExecute,'xml','nvarchar(max)'); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');''','''''DBCC FREEPROCCACHE ('''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '''');'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlySqlHandles = '''''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; ''','''''EXEC sp_BlitzCache @OnlyQueryHashes = '''''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''''; '''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''N/A''','''''N/A'''''); + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new LastCompletionTime column, add it. See Github #2377. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + ''')) AND name = ''LastCompletionTime'') + ALTER TABLE ' + @ObjectFullName + N' ADD LastCompletionTime DATETIME NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''LastCompletionTime''','''''LastCompletionTime'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new PlanGenerationNum column, add it. See Github #2514. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''PlanGenerationNum'') + ALTER TABLE ' + @ObjectFullName + N' ADD PlanGenerationNum BIGINT NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''PlanGenerationNum''','''''PlanGenerationNum'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + + /* If the table doesn't have the new Pattern column, add it */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''Pattern'') + ALTER TABLE ' + @ObjectFullName + N' ADD Pattern NVARCHAR(20) NULL;'; + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''Pattern''','''''Pattern'''''); + SET @StringToExecute = REPLACE(@StringToExecute,'''' + @ObjectFullName + '''','''''' + @ObjectFullName + ''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END + + IF @CheckDateOverride IS NULL + BEGIN + SET @CheckDateOverride = SYSDATETIMEOFFSET(); + END; + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputServerName + '.' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputServerName + '.' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, CAST(QueryPlan AS NVARCHAR(MAX)), NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 1, 4000); + PRINT SUBSTRING(@StringToExecute, 4001, 4000); + PRINT SUBSTRING(@StringToExecute, 8001, 4000); + PRINT SUBSTRING(@StringToExecute, 12001, 4000); + PRINT SUBSTRING(@StringToExecute, 16001, 4000); + PRINT SUBSTRING(@StringToExecute, 20001, 4000); + PRINT SUBSTRING(@StringToExecute, 24001, 4000); + PRINT SUBSTRING(@StringToExecute, 28001, 4000); + PRINT SUBSTRING(@StringToExecute, 32001, 4000); + PRINT SUBSTRING(@StringToExecute, 36001, 4000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + ELSE + BEGIN + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + ''') INSERT ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + SET @StringToExecute += N' AND SPID = @@SPID '; + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + END; + + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + IF @ValidOutputServer = 1 + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF @OutputTableName IN ('##BlitzCacheProcs','##BlitzCacheResults') + BEGIN + RAISERROR('OutputTableName is a reserved name for this procedure. We only use ##BlitzCacheProcs and ##BlitzCacheResults, please choose another table name.', 16, 0); + END; + ELSE + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' + + @OutputTableName + + ''') IS NOT NULL) DROP TABLE ' + @OutputTableName + ';' + + 'CREATE TABLE ' + + @OutputTableName + + ' (ID bigint NOT NULL IDENTITY(1,1), + ServerName NVARCHAR(258), + CheckDate DATETIMEOFFSET, + Version NVARCHAR(258), + QueryType NVARCHAR(258), + Warnings varchar(max), + DatabaseName sysname, + SerialDesiredMemory float, + SerialRequiredMemory float, + AverageCPU bigint, + TotalCPU bigint, + PercentCPUByType money, + CPUWeight money, + AverageDuration bigint, + TotalDuration bigint, + DurationWeight money, + PercentDurationByType money, + AverageReads bigint, + TotalReads bigint, + ReadWeight money, + PercentReadsByType money, + AverageWrites bigint, + TotalWrites bigint, + WriteWeight money, + PercentWritesByType money, + ExecutionCount bigint, + ExecutionWeight money, + PercentExecutionsByType money, + ExecutionsPerMinute money, + PlanCreationTime datetime,' + N' + PlanCreationTimeHours AS DATEDIFF(HOUR, PlanCreationTime, SYSDATETIME()), + LastExecutionTime datetime, + LastCompletionTime datetime, + PlanHandle varbinary(64), + [Remove Plan Handle From Cache] AS + CASE WHEN [PlanHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [PlanHandle], 1) + '');'' + ELSE ''N/A'' END, + SqlHandle varbinary(64), + [Remove SQL Handle From Cache] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''DBCC FREEPROCCACHE ('' + CONVERT(VARCHAR(128), [SqlHandle], 1) + '');'' + ELSE ''N/A'' END, + [SQL Handle More Info] AS + CASE WHEN [SqlHandle] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlySqlHandles = '''''' + CONVERT(VARCHAR(128), [SqlHandle], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryHash binary(8), + [Query Hash More Info] AS + CASE WHEN [QueryHash] IS NOT NULL + THEN ''EXEC sp_BlitzCache @OnlyQueryHashes = '''''' + CONVERT(VARCHAR(32), [QueryHash], 1) + ''''''; '' + ELSE ''N/A'' END, + QueryPlanHash binary(8), + StatementStartOffset int, + StatementEndOffset int, + PlanGenerationNum bigint, + MinReturnedRows bigint, + MaxReturnedRows bigint, + AverageReturnedRows money, + TotalReturnedRows bigint, + QueryText nvarchar(max), + QueryPlan xml, + NumberOfPlans int, + NumberOfDistinctPlans int, + MinGrantKB BIGINT, + MaxGrantKB BIGINT, + MinUsedGrantKB BIGINT, + MaxUsedGrantKB BIGINT, + PercentMemoryGrantUsed MONEY, + AvgMaxMemoryGrant MONEY, + MinSpills BIGINT, + MaxSpills BIGINT, + TotalSpills BIGINT, + AvgSpills MONEY, + QueryPlanCost FLOAT, + Pattern NVARCHAR(20), + JoinKey AS ServerName + Cast(CheckDate AS NVARCHAR(50)), + CONSTRAINT [PK_' + REPLACE(REPLACE(@OutputTableName,'[',''),']','') + '] PRIMARY KEY CLUSTERED(ID ASC));'; + SET @StringToExecute += N' INSERT ' + + @OutputTableName + + ' (ServerName, CheckDate, Version, QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, CPUWeight, AverageDuration, TotalDuration, DurationWeight, PercentDurationByType, AverageReads, TotalReads, ReadWeight, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, WriteWeight, PercentWritesByType, ExecutionCount, ExecutionWeight, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ) ' + + 'SELECT TOP (@Top) ' + + QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), '''') + ', @CheckDateOverride, ' + + QUOTENAME(CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)), '''') + ', ' + + ' QueryType, DatabaseName, AverageCPU, TotalCPU, PercentCPUByType, PercentCPU, AverageDuration, TotalDuration, PercentDuration, PercentDurationByType, AverageReads, TotalReads, PercentReads, PercentReadsByType, ' + + ' AverageWrites, TotalWrites, PercentWrites, PercentWritesByType, ExecutionCount, PercentExecutions, PercentExecutionsByType, ' + + ' ExecutionsPerMinute, PlanCreationTime, LastExecutionTime, LastCompletionTime, PlanHandle, SqlHandle, QueryHash, QueryPlanHash, StatementStartOffset, StatementEndOffset, PlanGenerationNum, MinReturnedRows, MaxReturnedRows, AverageReturnedRows, TotalReturnedRows, QueryText, QueryPlan, NumberOfPlans, NumberOfDistinctPlans, Warnings, ' + + ' SerialRequiredMemory, SerialDesiredMemory, MinGrantKB, MaxGrantKB, MinUsedGrantKB, MaxUsedGrantKB, PercentMemoryGrantUsed, AvgMaxMemoryGrant, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost, Pattern ' + + ' FROM ##BlitzCacheProcs ' + + ' WHERE 1=1 '; + + IF @MinimumExecutionCount IS NOT NULL + BEGIN + SET @StringToExecute += N' AND ExecutionCount >= @MinimumExecutionCount '; + END; + + IF @MinutesBack IS NOT NULL + BEGIN + SET @StringToExecute += N' AND LastCompletionTime >= DATEADD(MINUTE, @min_back, GETDATE() ) '; + END; + + SET @StringToExecute += N' AND SPID = @@SPID '; + + SELECT @StringToExecute += N' ORDER BY ' + CASE @SortOrder WHEN 'cpu' THEN N' TotalCPU ' + WHEN N'reads' THEN N' TotalReads ' + WHEN N'writes' THEN N' TotalWrites ' + WHEN N'duration' THEN N' TotalDuration ' + WHEN N'executions' THEN N' ExecutionCount ' + WHEN N'compiles' THEN N' PlanCreationTime ' + WHEN N'memory grant' THEN N' MaxGrantKB' + WHEN N'spills' THEN N' MaxSpills' + WHEN N'avg cpu' THEN N' AverageCPU' + WHEN N'avg reads' THEN N' AverageReads' + WHEN N'avg writes' THEN N' AverageWrites' + WHEN N'avg duration' THEN N' AverageDuration' + WHEN N'avg executions' THEN N' ExecutionsPerMinute' + WHEN N'avg memory grant' THEN N' AvgMaxMemoryGrant' + WHEN N'avg spills' THEN N' AvgSpills' + WHEN N'unused grant' THEN N' MaxGrantKB - MaxUsedGrantKB' + ELSE N' TotalCPU ' + END + N' DESC '; + + SET @StringToExecute += N' OPTION (RECOMPILE) ; '; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@StringToExecute, 0, 4000); + PRINT SUBSTRING(@StringToExecute, 4000, 8000); + PRINT SUBSTRING(@StringToExecute, 8000, 12000); + PRINT SUBSTRING(@StringToExecute, 12000, 16000); + PRINT SUBSTRING(@StringToExecute, 16000, 20000); + PRINT SUBSTRING(@StringToExecute, 20000, 24000); + PRINT SUBSTRING(@StringToExecute, 24000, 28000); + PRINT SUBSTRING(@StringToExecute, 28000, 32000); + PRINT SUBSTRING(@StringToExecute, 32000, 36000); + PRINT SUBSTRING(@StringToExecute, 36000, 40000); + PRINT SUBSTRING(@StringToExecute, 34000, 40000); + END; + EXEC sp_executesql @StringToExecute, N'@Top INT, @min_duration INT, @min_back INT, @CheckDateOverride DATETIMEOFFSET, @MinimumExecutionCount INT', @Top, @DurationFilter_i, @MinutesBack, @CheckDateOverride, @MinimumExecutionCount; + END; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); +END; /* End of writing results to table */ + END; /*Final End*/ GO diff --git a/sp_BlitzFirst.sql b/sp_BlitzFirst.sql index 135780d3f..00e1f3064 100644 --- a/sp_BlitzFirst.sql +++ b/sp_BlitzFirst.sql @@ -1,5 +1,5 @@ IF OBJECT_ID('dbo.sp_BlitzFirst') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;') + EXEC ('CREATE PROCEDURE dbo.sp_BlitzFirst AS RETURN 0;'); GO @@ -18,13 +18,18 @@ ALTER PROCEDURE [dbo].[sp_BlitzFirst] @OutputTableNamePerfmonStats NVARCHAR(256) = NULL , @OutputTableNameWaitStats NVARCHAR(256) = NULL , @OutputTableNameBlitzCache NVARCHAR(256) = NULL , + @OutputTableNameBlitzWho NVARCHAR(256) = NULL , + @OutputResultSets NVARCHAR(500) = N'BlitzWho_Start|Findings|FileStats|PerfmonStats|WaitStats|BlitzCache|BlitzWho_End' , @OutputTableRetentionDays TINYINT = 7 , @OutputXMLasNVARCHAR TINYINT = 0 , @FilterPlansByDatabase VARCHAR(MAX) = NULL , @CheckProcedureCache TINYINT = 0 , + @CheckServerInfo TINYINT = 1 , @FileLatencyThresholdMS INT = 100 , @SinceStartup TINYINT = 0 , @ShowSleepingSPIDs TINYINT = 0 , + @BlitzCacheSkipAnalysis BIT = 1 , + @MemoryGrantThresholdPct DECIMAL(5,2) = 15.00, @LogMessageCheckID INT = 38, @LogMessagePriority TINYINT = 1, @LogMessageFindingsGroup VARCHAR(50) = 'Logged Message', @@ -32,18 +37,26 @@ ALTER PROCEDURE [dbo].[sp_BlitzFirst] @LogMessageURL VARCHAR(200) = '', @LogMessageCheckDate DATETIMEOFFSET = NULL, @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH EXECUTE AS CALLER, RECOMPILE AS BEGIN SET NOCOUNT ON; +SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; +SELECT @Version = '8.26', @VersionDate = '20251002'; -IF @Help = 1 PRINT ' +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; + +IF @Help = 1 +BEGIN +PRINT ' sp_BlitzFirst from http://FirstResponderKit.org This script gives you a prioritized list of why your SQL Server is slow right now. @@ -74,7 +87,7 @@ https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ MIT License -Copyright (c) 2017 Brent Ozar Unlimited +Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -94,8 +107,9 @@ 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. -' - +'; +RETURN; +END; /* @Help = 1 */ RAISERROR('Setting up configuration variables',10,1) WITH NOWAIT; DECLARE @StringToExecute NVARCHAR(MAX), @@ -103,30 +117,52 @@ DECLARE @StringToExecute NVARCHAR(MAX), @Parm1 NVARCHAR(4000), @OurSessionID INT, @LineFeed NVARCHAR(10), - @StockWarningHeader NVARCHAR(500), - @StockWarningFooter NVARCHAR(100), - @StockDetailsHeader NVARCHAR(100), - @StockDetailsFooter NVARCHAR(100), + @StockWarningHeader NVARCHAR(MAX) = N'', + @StockWarningFooter NVARCHAR(MAX) = N'', + @StockDetailsHeader NVARCHAR(MAX) = N'', + @StockDetailsFooter NVARCHAR(MAX) = N'', @StartSampleTime DATETIMEOFFSET, @FinishSampleTime DATETIMEOFFSET, @FinishSampleTimeWaitFor DATETIME, + @AsOf1 DATETIMEOFFSET, + @AsOf2 DATETIMEOFFSET, @ServiceName sysname, @OutputTableNameFileStats_View NVARCHAR(256), @OutputTableNamePerfmonStats_View NVARCHAR(256), + @OutputTableNamePerfmonStatsActuals_View NVARCHAR(256), @OutputTableNameWaitStats_View NVARCHAR(256), @OutputTableNameWaitStats_Categories NVARCHAR(256), + @OutputTableCleanupDate DATE, @ObjectFullName NVARCHAR(2000), @BlitzWho NVARCHAR(MAX) = N'EXEC dbo.sp_BlitzWho @ShowSleepingSPIDs = ' + CONVERT(NVARCHAR(1), @ShowSleepingSPIDs) + N';', @BlitzCacheMinutesBack INT, - @BlitzCacheSortOrder VARCHAR(50), @UnquotedOutputServerName NVARCHAR(256) = @OutputServerName , @UnquotedOutputDatabaseName NVARCHAR(256) = @OutputDatabaseName , - @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName ; + @UnquotedOutputSchemaName NVARCHAR(256) = @OutputSchemaName , + @LocalServerName NVARCHAR(128) = CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)), + @dm_exec_query_statistics_xml BIT = 0, + @total_cpu_usage BIT = 0, + @get_thread_time_ms NVARCHAR(MAX) = N'', + @thread_time_ms FLOAT = 0, + @logical_processors 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 @OutputTableNameFileStats_View = QUOTENAME(@OutputTableNameFileStats + '_Deltas'), @OutputTableNamePerfmonStats_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Deltas'), + @OutputTableNamePerfmonStatsActuals_View = QUOTENAME(@OutputTableNamePerfmonStats + '_Actuals'), @OutputTableNameWaitStats_View = QUOTENAME(@OutputTableNameWaitStats + '_Deltas'), @OutputTableNameWaitStats_Categories = QUOTENAME(@OutputTableNameWaitStats + '_Categories'); @@ -137,13 +173,23 @@ SELECT @OutputTableNameFileStats = QUOTENAME(@OutputTableNameFileStats), @OutputTableNamePerfmonStats = QUOTENAME(@OutputTableNamePerfmonStats), @OutputTableNameWaitStats = QUOTENAME(@OutputTableNameWaitStats), + @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE), /* @OutputTableNameBlitzCache = QUOTENAME(@OutputTableNameBlitzCache), We purposely don't sanitize this because sp_BlitzCache will */ + /* @OutputTableNameBlitzWho = QUOTENAME(@OutputTableNameBlitzWho), We purposely don't sanitize this because sp_BlitzWho will */ @LineFeed = CHAR(13) + CHAR(10), - @StartSampleTime = SYSDATETIMEOFFSET(), - @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), - @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()), - @OurSessionID = @@SPID; + @OurSessionID = @@SPID, + @OutputType = UPPER(@OutputType); + +IF(@OutputType = 'NONE' AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) +BEGIN + RAISERROR('This procedure should be called with a value for all @Output* parameters, as @OutputType is set to NONE',12,1); + RETURN; +END; +IF UPPER(@OutputType) LIKE 'TOP 10%' SET @OutputType = 'Top10'; +IF @OutputType = 'Top10' SET @SinceStartup = 1; + +/* Logged Message - CheckID 38 */ IF @LogMessage IS NOT NULL BEGIN @@ -163,7 +209,7 @@ IF @LogMessage IS NOT NULL IF @OutputDatabaseName IS NULL AND EXISTS (SELECT * FROM sys.databases WHERE name = 'DBAtools') SET @OutputDatabaseName = '[DBAtools]'; - END + END; IF @OutputDatabaseName IS NULL OR @OutputSchemaName IS NULL OR @OutputTableName IS NULL OR NOT EXISTS ( SELECT * @@ -172,7 +218,7 @@ IF @LogMessage IS NOT NULL BEGIN RAISERROR('We have a hard time logging a message without a valid @OutputDatabaseName, @OutputSchemaName, and @OutputTableName to log it to.', 0, 1) WITH NOWAIT; RETURN; - END + END; IF @LogMessageCheckDate IS NULL SET @LogMessageCheckDate = SYSDATETIMEOFFSET(); SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -182,42 +228,36 @@ IF @LogMessage IS NOT NULL + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @LogMessageCheckDate, 121) + ''', @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)' + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, Details, URL) VALUES( ' + + ' @SrvName, @LogMessageCheckDate, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)'; - EXECUTE sp_executesql @StringToExecute, - N'@LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', - @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; + EXECUTE sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', + @LocalServerName, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageCheckDate, @LogMessageURL; RAISERROR('LogMessage saved to table. We have made a note of your activity. Keep up the good work.',10,1) WITH NOWAIT; RETURN; - END + END; IF @SinceStartup = 1 - SELECT @Seconds = 0, @ExpertMode = 1; - -IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SELECT @StartSampleTime = DATEADD(ms, AVG(-wait_time_ms), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.dm_os_wait_stats w - WHERE wait_type IN ('BROKER_TASK_STOP','DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', - 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_DISPATCHER_WAIT','XE_TIMER_EVENT') -ELSE IF @Seconds = 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' - SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() - FROM sys.databases - WHERE database_id = 2; -ELSE - SELECT @StartSampleTime = SYSDATETIMEOFFSET(), @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()); + BEGIN + SET @Seconds = 0 + IF @ExpertMode = 0 + SET @ExpertMode = 1 + END; + IF @OutputType = 'SCHEMA' BEGIN - SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)' + SELECT FieldList = '[Priority] TINYINT, [FindingsGroup] VARCHAR(50), [Finding] VARCHAR(200), [URL] VARCHAR(200), [Details] NVARCHAR(4000), [HowToStopIt] NVARCHAR(MAX), [QueryPlan] XML, [QueryText] NVARCHAR(MAX)'; -END +END; ELSE IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL BEGIN /* They want to look into the past. */ + SET @AsOf1= DATEADD(mi, -15, @AsOf); + SET @AsOf2= DATEADD(mi, +15, @AsOf); SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName @@ -228,28 +268,110 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableName - + ' WHERE CheckDate >= DATEADD(mi, -15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' - + ' AND CheckDate <= DATEADD(mi, 15, CONVERT(DATETIMEOFFSET, ''' + CAST(@AsOf AS NVARCHAR(100)) + '''))' + + ' WHERE CheckDate >= @AsOf1' + + ' AND CheckDate <= @AsOf2' + ' /*ORDER BY CheckDate, Priority , FindingsGroup , Finding , Details*/;'; - EXEC(@StringToExecute); + EXEC sp_executesql @StringToExecute, + N'@AsOf1 DATETIMEOFFSET, @AsOf2 DATETIMEOFFSET', + @AsOf1, @AsOf2 -END /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ +END; /* IF @AsOf IS NOT NULL AND @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL */ ELSE IF @LogMessage IS NULL /* IF @OutputType = 'SCHEMA' */ BEGIN /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_Start%' BEGIN IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; ELSE BEGIN - EXEC (@BlitzWho) + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ + + /* Set start/finish times AFTER sp_BlitzWho runs. For more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2244 */ + IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + /* Use the most accurate (but undocumented) DMV if it's available: */ + IF EXISTS(SELECT * FROM sys.all_columns ac WHERE ac.object_id = OBJECT_ID('sys.dm_cloud_database_epoch') AND ac.name = 'last_role_transition_time') + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),last_role_transition_time) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.dm_cloud_database_epoch; + ELSE + WITH WaitTimes AS ( + SELECT wait_type, wait_time_ms, + NTILE(3) OVER(ORDER BY wait_time_ms) AS grouper + FROM sys.dm_os_wait_stats w + WHERE wait_type IN ('DIRTY_PAGE_POLL','HADR_FILESTREAM_IOMGR_IOCOMPLETION','LAZYWRITER_SLEEP', + 'LOGMGR_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH','XE_TIMER_EVENT') + ) + SELECT @StartSampleTime = DATEADD(mi, AVG(-wait_time_ms / 1000 / 60), SYSDATETIMEOFFSET()), @FinishSampleTime = SYSDATETIMEOFFSET() + FROM WaitTimes + WHERE grouper = 2; END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ - + ELSE IF @Seconds = 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SELECT @StartSampleTime = DATEADD(MINUTE,DATEDIFF(MINUTE, GETDATE(), GETUTCDATE()),create_date) , @FinishSampleTime = SYSDATETIMEOFFSET() + FROM sys.databases + WHERE database_id = 2; + ELSE + SELECT @StartSampleTime = SYSDATETIMEOFFSET(), + @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), + @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()); + + + SELECT @logical_processors = COUNT(*) + FROM sys.dm_os_schedulers + WHERE status = 'VISIBLE ONLINE'; + + IF EXISTS + ( + + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.dm_os_schedulers') + AND ac.name = 'total_cpu_usage_ms' + + ) + BEGIN + + SELECT + @total_cpu_usage = 1, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_cpu_usage_ms) + ) + FROM sys.dm_os_schedulers AS s + WHERE s.status = ''VISIBLE ONLINE'' + AND s.is_online = 1 + OPTION(RECOMPILE); + '; + + END + ELSE + BEGIN + SELECT + @total_cpu_usage = 0, + @get_thread_time_ms += + N' + SELECT + @thread_time_ms = + CONVERT + ( + FLOAT, + SUM(s.total_worker_time / 1000.) + ) + FROM sys.dm_exec_query_stats AS s + OPTION(RECOMPILE); + '; + END RAISERROR('Now starting diagnostic analysis',10,1) WITH NOWAIT; @@ -257,7 +379,7 @@ BEGIN We start by creating #BlitzFirstResults. It's a temp table that will store the results from our checks. Throughout the rest of this stored procedure, we're running a series of checks looking for dangerous things inside the SQL - Server. When we find a problem, we insert rows into #BlitzResults. At the + Server. When we find a problem, we insert rows into the temp table. At the end, we return these results to the end user. #BlitzFirstResults has a CheckID field, but there's no Check table. As we do @@ -267,6 +389,7 @@ BEGIN a tool that relies on the output of sp_BlitzFirst. */ + IF OBJECT_ID('tempdb..#BlitzFirstResults') IS NOT NULL DROP TABLE #BlitzFirstResults; CREATE TABLE #BlitzFirstResults @@ -277,7 +400,7 @@ BEGIN FindingsGroup VARCHAR(50) NOT NULL, Finding VARCHAR(200) NOT NULL, URL VARCHAR(200) NULL, - Details NVARCHAR(4000) NULL, + Details NVARCHAR(MAX) NULL, HowToStopIt NVARCHAR(MAX) NULL, QueryPlan [XML] NULL, QueryText NVARCHAR(MAX) NULL, @@ -294,11 +417,20 @@ BEGIN QueryStatsFirstID INT NULL, PlanHandle VARBINARY(64) NULL, DetailsInt INT NULL, + QueryHash BINARY(8) ); IF OBJECT_ID('tempdb..#WaitStats') IS NOT NULL DROP TABLE #WaitStats; - CREATE TABLE #WaitStats (Pass TINYINT NOT NULL, wait_type NVARCHAR(60), wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT, SampleTime DATETIMEOFFSET); + CREATE TABLE #WaitStats ( + Pass TINYINT NOT NULL, + wait_type NVARCHAR(60), + wait_time_ms BIGINT, + thread_time_ms FLOAT, + signal_wait_time_ms BIGINT, + waiting_tasks_count BIGINT, + SampleTime datetimeoffset + ); IF OBJECT_ID('tempdb..#FileStats') IS NOT NULL DROP TABLE #FileStats; @@ -375,6 +507,17 @@ BEGIN DROP TABLE #FilterPlansByDatabase; CREATE TABLE #FilterPlansByDatabase (DatabaseID INT PRIMARY KEY CLUSTERED); + IF OBJECT_ID ('tempdb..#checkversion') IS NOT NULL + DROP TABLE #checkversion; + CREATE TABLE #checkversion ( + version NVARCHAR(128), + common_version AS SUBSTRING(version, 1, CHARINDEX('.', version) + 1 ), + major AS PARSENAME(CONVERT(VARCHAR(32), version), 4), + minor AS PARSENAME(CONVERT(VARCHAR(32), version), 3), + build AS PARSENAME(CONVERT(VARCHAR(32), version), 2), + revision AS PARSENAME(CONVERT(VARCHAR(32), version), 1) + ); + IF OBJECT_ID('tempdb..##WaitCategories') IS NULL BEGIN /* We reuse this one by default rather than recreate it every time. */ @@ -384,9 +527,9 @@ BEGIN WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0 ); - END /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ + END; /* IF OBJECT_ID('tempdb..##WaitCategories') IS NULL */ - IF 504 <> (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) + IF 527 > (SELECT COALESCE(SUM(1),0) FROM ##WaitCategories) BEGIN TRUNCATE TABLE ##WaitCategories; INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('ASYNC_IO_COMPLETION','Other Disk IO',0); @@ -414,20 +557,21 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('BROKER_TRANSMITTER','Service Broker',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHECKPOINT_QUEUE','Idle',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CHKPT','Tran Log IO',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_AUTO_EVENT','SQL CLR',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_CRST','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_JOIN','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MANUAL_EVENT','SQL CLR',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MEMORY_SPY','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_MONITOR','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_READER','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_RWLOCK_WRITER','SQL CLR',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_SEMAPHORE','SQL CLR',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLR_TASK_START','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CLRHOST_STATE_ACCESS','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMPARTITIONED','Memory',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CMEMTHREAD','Memory',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXPACKET','Parallelism',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('CXCONSUMER','Parallelism',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_EVENT','Mirroring',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_DBM_MUTEX','Mirroring',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_EVENTS_QUEUE','Mirroring',1); @@ -435,7 +579,9 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRROR_WORKER_QUEUE','Mirroring',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DBMIRRORING_CMD','Mirroring',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_POLL','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DIRTY_PAGE_TABLE_LOCK','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DISPATCHER_QUEUE_SEMAPHORE','Other',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DPT_ENTRY_LOCK','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC','Transaction',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_ABORT_REQUEST','Transaction',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('DTC_RESOLVE','Transaction',0); @@ -456,7 +602,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_COMPROWSET_RWLOCK','Full Text Search',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_RWLOCK','Full Text Search',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTS_SCHEDULER_IDLE_WAIT','Idle',1); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSHC_MUTEX','Full Text Search',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_IFTSISM_MUTEX','Full Text Search',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE','Full Text Search',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('FT_MASTER_MERGE_COORDINATOR','Full Text Search',0); @@ -487,7 +633,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSEEDING_LIST','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_DBSTATECHANGE_SYNC','Replication',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FABRIC_CALLBACK','Replication',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_BLOCK_FLUSH','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_CLOSE','Replication',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('HADR_FILESTREAM_FILE_REQUEST','Replication',0); @@ -600,6 +746,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X','Lock',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_ABORT_BLOCKERS','Lock',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LCK_M_X_LOW_PRIORITY','Lock',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOG_RATE_GOVERNOR','Tran Log IO',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGBUFFER','Tran Log IO',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR','Tran Log IO',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('LOGMGR_FLUSH','Tran Log IO',0); @@ -625,7 +772,15 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_NL','Buffer Latch',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_SH','Buffer Latch',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PAGELATCH_UP','Buffer Latch',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_DRAIN_WORKER','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_FLOW_CONTROL','Replication',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_LOG_CACHE','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_LIST','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_TRAN_TURN','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_SYNC','Replication',1); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PARALLEL_REDO_WORKER_WAIT_WORK','Replication',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POOL_LOG_RATE_GOVERNOR','Log Rate Governor',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('POPULATE_LOCK_ORDINALS','Idle',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_ABR','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPMEDIA','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_CLOSEBACKUPTAPE','Preemptive',0); @@ -816,7 +971,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CALLBACKEXECUTE','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_FILE_OPEN','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_CX_HTTP_CALL','Preemptive',0); - INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_DISPATCHER','Preemptive',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_ENGINEINIT','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_GETTARGETSTATE','Preemptive',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('PREEMPTIVE_XE_SESSIONCOMMIT','Preemptive',0); @@ -868,6 +1023,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_TEMPDBSTARTUP','Idle',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SLEEP_WORKSPACE_ALLOCATEPAGE','Idle',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_SCHEDULER_YIELD','CPU',0); + INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SOS_WORK_DISPATCHER','Idle',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SP_SERVER_DIAGNOSTICS_SLEEP','Other',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_APPDOMAIN','SQL CLR',0); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('SQLCLR_ASSEMBLY','SQL CLR',0); @@ -904,7 +1060,7 @@ BEGIN INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_DISPATCHER_WAIT','Idle',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_LIVE_TARGET_TVF','Other',1); INSERT INTO ##WaitCategories(WaitType, WaitCategory, Ignorable) VALUES ('XE_TIMER_EVENT','Idle',1); - END /* IF SELECT SUM(1) FROM ##WaitCategories <> 504 */ + END; /* IF SELECT SUM(1) FROM ##WaitCategories <> 527 */ @@ -912,10 +1068,11 @@ BEGIN DROP TABLE #MasterFiles; CREATE TABLE #MasterFiles (database_id INT, file_id INT, type_desc NVARCHAR(50), name NVARCHAR(255), physical_name NVARCHAR(255), size BIGINT); /* Azure SQL Database doesn't have sys.master_files, so we have to build our own. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;' + IF (SERVERPROPERTY('EngineEdition') = 5 /*(SERVERPROPERTY('Edition')) = 'SQL Azure' */ + AND (OBJECT_ID('sys.master_files') IS NULL)) + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT DB_ID(), file_id, type_desc, name, physical_name, size FROM sys.database_files;'; ELSE - SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;' + SET @StringToExecute = 'INSERT INTO #MasterFiles (database_id, file_id, type_desc, name, physical_name, size) SELECT database_id, file_id, type_desc, name, physical_name, size FROM sys.master_files;'; EXEC(@StringToExecute); IF @FilterPlansByDatabase IS NOT NULL @@ -925,8 +1082,8 @@ BEGIN INSERT INTO #FilterPlansByDatabase (DatabaseID) SELECT database_id FROM sys.databases - WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb') - END + WHERE [name] NOT IN ('master', 'model', 'msdb', 'tempdb'); + END; ELSE BEGIN SET @FilterPlansByDatabase = @FilterPlansByDatabase + ',' @@ -939,12 +1096,57 @@ BEGIN WHERE CHARINDEX(',', @FilterPlansByDatabase, t + 1) > 0 ) INSERT #FilterPlansByDatabase (DatabaseID) - SELECT SUBSTRING(@FilterPlansByDatabase, f, t - f) + SELECT DISTINCT db.database_id FROM a + INNER JOIN sys.databases db ON LTRIM(RTRIM(SUBSTRING(@FilterPlansByDatabase, a.f, a.t - a.f))) = db.name WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL - OPTION (MAXRECURSION 0) - END - END + OPTION (MAXRECURSION 0); + END; + END; + + IF OBJECT_ID('tempdb..#ReadableDBs') IS NOT NULL + DROP TABLE #ReadableDBs; + CREATE TABLE #ReadableDBs ( + database_id INT + ); + + IF 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; + + SET @StringToExecute = 'INSERT INTO #ReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'; + EXEC(@StringToExecute); + + END + + DECLARE @v DECIMAL(6,2), + @build INT, + @memGrantSortSupported BIT = 1; + + RAISERROR (N'Determining SQL Server version.',0,1) WITH NOWAIT; + + INSERT INTO #checkversion (version) + SELECT CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)) + OPTION (RECOMPILE); + + + SELECT @v = common_version , + @build = build + FROM #checkversion + OPTION (RECOMPILE); + + IF (@v < 11) + OR (@v = 11 AND @build < 6020) + OR (@v = 12 AND @build < 5000) + OR (@v = 13 AND @build < 1601) + SET @memGrantSortSupported = 0; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + AND ((@v = 13 AND @build >= 5337) /* This DMF causes assertion errors: https://support.microsoft.com/en-us/help/4490136/fix-assertion-error-occurs-when-you-use-sys-dm-exec-query-statistics-x */ + OR (@v = 14 AND @build >= 3162) + OR (@v >= 15) + OR (@v <= 12)) /* Azure */ + SET @dm_exec_query_statistics_xml = 1; SET @StockWarningHeader = '', - @StockDetailsHeader = ''; + SELECT @StockWarningFooter = @StockWarningFooter + @LineFeed + @LineFeed + '-- ?>', + @StockDetailsHeader = @StockDetailsHeader + ''; /* Get the instance name to use as a Perfmon counter prefix. */ - IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure' + IF SERVERPROPERTY('EngineEdition') IN (5, 8) /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) = 'SQL Azure'*/ SELECT TOP 1 @ServiceName = LEFT(object_name, (CHARINDEX(':', object_name) - 1)) FROM sys.dm_os_performance_counters; ELSE BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;' + SET @StringToExecute = 'INSERT INTO #PerfmonStats(object_name, Pass, SampleTime, counter_name, cntr_type) SELECT CASE WHEN @@SERVICENAME = ''MSSQLSERVER'' THEN ''SQLServer'' ELSE ''MSSQL$'' + @@SERVICENAME END, 0, SYSDATETIMEOFFSET(), ''stuffing'', 0 ;'; EXEC(@StringToExecute); SELECT @ServiceName = object_name FROM #PerfmonStats; DELETE #PerfmonStats; - END + END; /* Build a list of queries that were run in the last 10 seconds. We're looking for the death-by-a-thousand-small-cuts scenario @@ -989,7 +1191,7 @@ BEGIN SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 FROM sys.dm_exec_query_stats qs WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END + END; ELSE BEGIN SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) @@ -999,8 +1201,8 @@ BEGIN INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) AND attr.attribute = ''dbid'';'; - END - END + END; + END; ELSE BEGIN IF @FilterPlansByDatabase IS NULL @@ -1009,7 +1211,7 @@ BEGIN SELECT [sql_handle], 1 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 FROM sys.dm_exec_query_stats qs WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET()));'; - END + END; ELSE BEGIN SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) @@ -1019,15 +1221,15 @@ BEGIN INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID WHERE qs.last_execution_time >= (DATEADD(ss, -10, SYSDATETIMEOFFSET())) AND attr.attribute = ''dbid'';'; - END - END + END; + END; EXEC(@StringToExecute); /* Get the totals for the entire plan cache */ INSERT INTO #QueryStats (Pass, SampleTime, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time) SELECT -1 AS Pass, SYSDATETIMEOFFSET(), SUM(execution_count), SUM(total_worker_time), SUM(total_physical_reads), SUM(total_logical_writes), SUM(total_logical_reads), SUM(total_clr_time), SUM(total_elapsed_time), MIN(creation_time) FROM sys.dm_exec_query_stats qs; - END /*IF @CheckProcedureCache = 1 */ + END; /*IF @CheckProcedureCache = 1 */ IF EXISTS (SELECT * @@ -1037,197 +1239,241 @@ BEGIN INNER JOIN tempdb.sys.all_columns col3 ON obj.object_id = col3.object_id AND col3.name = 'instance_name' WHERE obj.name LIKE '%CustomPerfmonCounters%') BEGIN - SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters' + SET @StringToExecute = 'INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) SELECT [object_name],[counter_name],[instance_name] FROM #CustomPerfmonCounters'; EXEC(@StringToExecute); - END + END; ELSE BEGIN /* Add our default Perfmon counters */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total') - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL) + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Forwarded Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page compression attempts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Page Splits/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Skipped Ghosted Records/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Table Lock Escalations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables Created/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Group','Active Hadr Threads','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Received from Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Bytes Sent to Transport/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control Time (ms/sec)','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Flow Control/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Resent Messages/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Availability Replica','Sends to Replica/sec','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page life expectancy', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page reads/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page writes/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Readahead pages/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Target pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Total pages', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Active Transactions','_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Control Delay', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Database Flow Controls/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commit Time', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Group Commits/Sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Pending Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Apply Ready Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Compression Cache misses/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log remaining for undo', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Log Send Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Recovery Queue', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo blocked/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redo Bytes Remaining', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Database Replica','Redone Bytes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Bytes Flushed/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Growths', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Pool LogWriter Pushes/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Log Shrinks', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','Write Transactions/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Databases','XTP Memory Used (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Distributed Query', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','DTC calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','Extended Procedures', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Exec Statistics','OLEDB calls', 'Execs in progress'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Active Temp Tables', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logins/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Logouts/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Mars Deadlocks', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','Processes blocked', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Number of Deadlocks/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Memory Grants Pending', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Errors','Errors/sec', '_Total'); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Batch Requests/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Forced Parameterizations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Guided plan executions/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Attention rate', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Query optimizations/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Workload Group Stats','Suboptimal plans/sec',NULL); /* Below counters added by Jefferson Elias */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL) + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Worktables From Cache Ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Database pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Stolen pages',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Granted Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Maximum Workspace Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Target Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Memory Manager','Total Server Memory (KB)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Buffer cache hit ratio base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Checkpoint pages/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Free list stalls/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Lazy writes/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Auto-Param Attempts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Failed Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Safe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','Unsafe Auto-Params/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Workfiles Created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':General Statistics','User Connections',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Average Latch Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Latch Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Latches','Total Latch Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Average Wait Time Base',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Requests/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Timeouts/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Wait Time (ms)',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Locks','Lock Waits/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Transactions','Longest Transaction Running Time',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Full Scans/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Access Methods','Index Searches/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Buffer Manager','Page lookups/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':Cursor Manager by Type','Active cursors',NULL); /* Below counters are for In-Memory OLTP (Hekaton), which have a different naming convention. - And yes, they actually hard-coded the version numbers into the counters. + And yes, they actually hard-coded the version numbers into the counters, and SQL 2019 still says 2017, oddly. For why, see: https://connect.microsoft.com/SQLServer/feedback/details/817216/xtp-perfmon-counters-should-appear-under-sql-server-perfmon-counter-group */ - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL) - END + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2014 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2016 XTP Transactions','Transactions created/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows removed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Cursors','Expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Garbage Collection','Rows processed/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP IO Governor','Io Issued/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom expired rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Phantom Processor','Phantom rows touched/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log bytes written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transaction Log','Log records written/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted by user/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions aborted/sec',NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES ('SQL Server 2017 XTP Transactions','Transactions created/sec',NULL); + END; + + + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. After we finish doing our checks, we'll take another sample and compare them. */ RAISERROR('Capturing first pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - owt.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - 0 AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 1 AS Pass, - CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, - os.wait_type, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + CASE @Seconds + WHEN 0 + THEN 0 + ELSE @thread_time_ms + END AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + owt.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + 0 AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 1 AS Pass, + CASE @Seconds WHEN 0 THEN @StartSampleTime ELSE SYSDATETIMEOFFSET() END AS SampleTime, + os.wait_type, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) END AS sum_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) END AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) END AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; + + SET @StringToExecute = @StringToExecute + N' ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 ) GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; - - + ORDER BY sum_wait_time_ms DESC;' + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 1 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 1 + OPTION(RECOMPILE); + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc) SELECT @@ -1258,160 +1504,234 @@ BEGIN FROM #PerfmonCounters counters INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); + + /* For Github #2743: */ + CREATE TABLE #TempdbOperationalStats (object_id BIGINT PRIMARY KEY CLUSTERED, + forwarded_fetch_count BIGINT); + INSERT INTO #TempdbOperationalStats (object_id, forwarded_fetch_count) + SELECT object_id, forwarded_fetch_count + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID('tempdb'), NULL, NULL, NULL) os + WHERE os.database_id = DB_ID('tempdb') + AND os.forwarded_fetch_count > 100; + + + /* If they want to run sp_BlitzWho and export to table, go for it. */ + IF @OutputTableNameBlitzWho IS NOT NULL + AND @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('Logging sp_BlitzWho to table',10,1) WITH NOWAIT; + EXEC sp_BlitzWho @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzWho, @CheckDateOverride = @StartSampleTime, @OutputTableRetentionDays = @OutputTableRetentionDays; + END RAISERROR('Beginning investigatory queries',10,1) WITH NOWAIT; /* Maintenance Tasks Running - Backup Running - CheckID 1 */ IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 1 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Backup Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/backups/' AS URL, - 'Backup 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)) + '. ' - + CASE WHEN COALESCE(s.nt_user_name, s.login_name) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_user_name, s.login_name) + ' ') ELSE '' END AS Details, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'BACKUP%' - AND r.start_time <= DATEADD(minute, -5, GETDATE()); + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 1',10,1) WITH NOWAIT; + END + + IF EXISTS(SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 300000 AND command LIKE 'BACKUP%') + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 1 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'Backup Running' AS Finding, + 'https://www.brentozar.com/askbrent/backups/' AS URL, + 'Backup 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) ' + @LineFeed + + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete, has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '. ' + @LineFeed + + CASE WHEN COALESCE(s.nt_username, s.loginame) IS NOT NULL THEN (' Login: ' + COALESCE(s.nt_username, s.loginame) + ' ') ELSE '' END AS Details, + 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.[hostname] AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) 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.sysprocesses AS s ON r.session_id = s.spid AND s.ecid = 0 + INNER JOIN + ( + SELECT DISTINCT + t.request_session_id, + t.resource_database_id + FROM sys.dm_tran_locks AS t + WHERE t.resource_type = N'DATABASE' + AND t.request_mode = N'S' + AND t.request_status = N'GRANT' + AND t.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE' + ) AS db ON s.spid = db.request_session_id AND s.dbid = db.resource_database_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'BACKUP%' + AND r.start_time <= DATEADD(minute, -5, GETDATE()) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END /* If there's a backup running, add details explaining how long full backup has been taking in the last month. */ - IF @Seconds > 0 AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure' + IF @Seconds > 0 AND SERVERPROPERTY('EngineEdition') <> 5 /*CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) <> 'SQL Azure'*/ BEGIN SET @StringToExecute = 'UPDATE #BlitzFirstResults SET Details = Details + '' Over the last 60 days, the full backup usually takes '' + CAST((SELECT AVG(DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date)) FROM msdb.dbo.backupset bs WHERE abr.DatabaseName = bs.database_name AND bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL) AS NVARCHAR(100)) + '' minutes.'' FROM #BlitzFirstResults abr WHERE abr.CheckID = 1 AND EXISTS (SELECT * FROM msdb.dbo.backupset bs WHERE bs.type = ''D'' AND bs.backup_start_date > DATEADD(dd, -60, SYSDATETIMEOFFSET()) AND bs.backup_finish_date IS NOT NULL AND abr.DatabaseName = bs.database_name AND DATEDIFF(mi, bs.backup_start_date, bs.backup_finish_date) > 1)'; EXEC(@StringToExecute); - END + END; /* Maintenance Tasks Running - DBCC CHECK* Running - CheckID 2 */ - IF @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%') - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 2 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'DBCC CHECK* Running' AS Finding, - 'http://www.BrentOzar.com/askbrent/dbcc/' AS URL, - 'Corruption check 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) 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, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id - FROM sys.dm_tran_locks l - INNER JOIN sys.databases d ON l.resource_database_id = d.database_id - WHERE l.resource_type = N'DATABASE' - AND l.request_mode = N'S' - AND l.request_status = N'GRANT' - AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS t - WHERE r.command LIKE 'DBCC%' - AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%'; + IF @Seconds > 0 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 2',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'DBCC%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 2 AS CheckID, + 1 AS Priority, + 'Maintenance Tasks Running' AS FindingGroup, + 'DBCC CHECK* Running' AS Finding, + 'https://www.brentozar.com/askbrent/dbcc/' AS URL, + 'Corruption check 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) 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, + s.login_name AS LoginName, + 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, + 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 (SELECT DISTINCT l.request_session_id, l.resource_database_id + FROM sys.dm_tran_locks l + INNER JOIN sys.databases d ON l.resource_database_id = d.database_id + WHERE l.resource_type = N'DATABASE' + AND l.request_mode = N'S' + AND l.request_status = N'GRANT' + AND l.request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) pl + OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS t + WHERE r.command LIKE 'DBCC%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%dm_db_index_physical_stats%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%ALTER INDEX%' + AND CAST(t.text AS NVARCHAR(4000)) NOT LIKE '%fileproperty%' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END /* Maintenance Tasks Running - Restore Running - CheckID 3 */ IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 3 AS CheckID, - 1 AS Priority, - 'Maintenance Tasks Running' AS FindingGroup, - 'Restore Running' AS Finding, - 'http://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, - 'KILL ' + CAST(r.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - 0 AS OpenTransactionCount - 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 ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE r.command LIKE 'RESTORE%'; + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 3',10,1) WITH NOWAIT; + END + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE command LIKE 'RESTORE%' AND total_elapsed_time > 5000) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) + SELECT 3 AS CheckID, + 1 AS Priority, + '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), + (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, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + 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 + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT') AS db ON s.session_id = db.request_session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE r.command LIKE 'RESTORE%' + AND s.program_name <> 'SQL Server Log Shipping' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END /* SQL Server Internal Maintenance - Database File Growing - CheckID 4 */ IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) - SELECT 4 AS CheckID, - 1 AS Priority, - 'SQL Server Internal Maintenance' AS FindingGroup, - 'Database File Growing' AS Finding, - 'http://www.BrentOzar.com/go/instant' AS URL, - 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, - 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out http://www.BrentOzar.com/go/instant for details.' AS HowToStopIt, - pl.query_plan AS QueryPlan, - r.start_time AS StartTime, - s.login_name AS LoginName, - s.nt_user_name AS NTUserName, - s.[program_name] AS ProgramName, - s.[host_name] AS HostName, - NULL AS DatabaseID, - NULL AS DatabaseName, - 0 AS OpenTransactionCount - FROM sys.dm_os_waiting_tasks t - INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id - INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id - CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl - WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 4',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) + SELECT 4 AS CheckID, + 1 AS Priority, + 'SQL Server Internal Maintenance' AS FindingGroup, + 'Database File Growing' AS Finding, + 'https://www.brentozar.com/go/instant' AS URL, + 'SQL Server is waiting for Windows to provide storage space for a database restore, a data file growth, or a log file growth. This task has been running since ' + CAST(r.start_time AS NVARCHAR(100)) + '.' + @LineFeed + 'Check the query plan (expert mode) to identify the database involved.' AS Details, + 'Unfortunately, you can''t stop this, but you can prevent it next time. Check out https://www.brentozar.com/go/instant for details.' AS HowToStopIt, + pl.query_plan AS QueryPlan, + r.start_time AS StartTime, + s.login_name AS LoginName, + s.nt_user_name AS NTUserName, + s.[program_name] AS ProgramName, + s.[host_name] AS HostName, + NULL AS DatabaseID, + NULL AS DatabaseName, + 0 AS OpenTransactionCount + FROM sys.dm_os_waiting_tasks t + INNER JOIN sys.dm_exec_connections c ON t.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON t.session_id = r.session_id + INNER JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) pl + WHERE t.wait_type = 'PREEMPTIVE_OS_WRITEFILEGATHER' + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs); + END /* Query Problems - Long-Running Query Blocking Others - CheckID 5 */ - IF @@VERSION NOT LIKE '%Azure%' AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ AND @Seconds > 0 AND EXISTS(SELECT * FROM sys.dm_os_waiting_tasks WHERE wait_type LIKE 'LCK%' AND wait_duration_ms > 30000) BEGIN - SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount) + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 5',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, QueryHash) SELECT 5 AS CheckID, 1 AS Priority, ''Query Problems'' AS FindingGroup, ''Long-Running Query Blocking Others'' AS Finding, - ''http://www.BrentOzar.com/go/blocking'' AS URL, + ''https://www.brentozar.com/go/blocking'' AS URL, ''Query in '' + COALESCE(DB_NAME(COALESCE((SELECT TOP 1 dbid FROM sys.dm_exec_sql_text(r.sql_handle)), - (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows:'' ' + (SELECT TOP 1 t.dbid FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) t WHERE spBlocker.spid = tBlocked.blocking_session_id))), ''(Unknown)'') + '' has a last request start time of '' + CAST(s.last_request_start_time AS NVARCHAR(100)) + ''. Query follows: ' + @LineFeed + @LineFeed + - '+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), + '''+ CAST(COALESCE((SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(r.sql_handle)), (SELECT TOP 1 [text] FROM master..sysprocesses spBlocker CROSS APPLY sys.dm_exec_sql_text(spBlocker.sql_handle) WHERE spBlocker.spid = tBlocked.blocking_session_id), '''') AS NVARCHAR(2000)) AS Details, ''KILL '' + CAST(tBlocked.blocking_session_id AS NVARCHAR(100)) + '';'' AS HowToStopIt, (SELECT TOP 1 query_plan FROM sys.dm_exec_query_plan(r.plan_handle)) AS QueryPlan, @@ -1424,24 +1744,33 @@ BEGIN s.[host_name] AS HostName, r.[database_id] AS DatabaseID, DB_NAME(r.database_id) AS DatabaseName, - 0 AS OpenTransactionCount + 0 AS OpenTransactionCount, + r.query_hash FROM sys.dm_os_waiting_tasks tBlocked INNER JOIN sys.dm_exec_sessions s ON tBlocked.blocking_session_id = s.session_id LEFT OUTER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000;' + WHERE tBlocked.wait_type LIKE ''LCK%'' AND tBlocked.wait_duration_ms > 30000 + /* And the blocking session ID is not blocked by anyone else: */ + AND NOT EXISTS(SELECT * FROM sys.dm_os_waiting_tasks tBlocking WHERE s.session_id = tBlocking.session_id AND tBlocking.session_id <> tBlocking.blocking_session_id AND tBlocking.blocking_session_id IS NOT NULL) + AND r.database_id NOT IN (SELECT database_id FROM #ReadableDBs);'; EXECUTE sp_executesql @StringToExecute; - END - - /* Query Problems - Plan Cache Erased Recently */ + END; + + /* Query Problems - Plan Cache Erased Recently - CheckID 7 */ IF DATEADD(mi, -15, SYSDATETIME()) < (SELECT TOP 1 creation_time FROM sys.dm_exec_query_stats ORDER BY creation_time) BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 7',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT TOP 1 7 AS CheckID, 50 AS Priority, 'Query Problems' AS FindingGroup, 'Plan Cache Erased Recently' AS Finding, - 'http://www.BrentOzar.com/askbrent/plan-cache-erased-recently/' AS URL, + 'https://www.brentozar.com/askbrent/plan-cache-erased-recently/' AS URL, 'The oldest query in the plan cache was created at ' + CAST(creation_time AS NVARCHAR(50)) + '. ' + @LineFeed + @LineFeed + 'This indicates that someone ran DBCC FREEPROCCACHE at that time,' + @LineFeed + 'Giving SQL Server temporary amnesia. Now, as queries come in,' + @LineFeed @@ -1449,99 +1778,193 @@ BEGIN + 'plans and put them in cache again. This causes high CPU loads.' AS Details, 'Find who did that, and stop them from doing it again.' AS HowToStopIt FROM sys.dm_exec_query_stats - ORDER BY creation_time + ORDER BY creation_time; END; /* Query Problems - Sleeping Query with Open Transactions - CheckID 8 */ IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) - SELECT 8 AS CheckID, - 50 AS Priority, - 'Query Problems' AS FindingGroup, - 'Sleeping Query with Open Transactions' AS Finding, - 'http://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, - 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.[host_name] + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_request_end_time AS NVARCHAR(100)) + '. ' AS Details, - 'KILL ' + CAST(s.session_id AS NVARCHAR(100)) + ';' AS HowToStopIt, - s.last_request_start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, - sessions_with_transactions.open_transaction_count AS OpenTransactionCount - FROM (SELECT session_id, SUM(open_transaction_count) AS open_transaction_count FROM sys.dm_exec_requests WHERE open_transaction_count > 0 GROUP BY session_id) AS sessions_with_transactions - INNER JOIN sys.dm_exec_sessions s ON sessions_with_transactions.session_id = s.session_id - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE s.status = 'sleeping' - AND s.last_request_end_time < DATEADD(ss, -10, SYSDATETIME()) - AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.session_id - AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 8',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 8 AS CheckID, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Sleeping Query with Open Transactions' AS Finding, + 'https://www.brentozar.com/askbrent/sleeping-query-with-open-transactions/' AS URL, + 'Database: ' + DB_NAME(db.resource_database_id) + @LineFeed + 'Host: ' + s.hostname + @LineFeed + 'Program: ' + s.[program_name] + @LineFeed + 'Asleep with open transactions and locks since ' + CAST(s.last_batch AS NVARCHAR(100)) + '. ' AS Details, + 'KILL ' + CAST(s.spid AS NVARCHAR(100)) + ';' AS HowToStopIt, + s.last_batch AS StartTime, + s.loginame AS LoginName, + s.nt_username AS NTUserName, + s.[program_name] AS ProgramName, + s.hostname AS HostName, + db.[resource_database_id] AS DatabaseID, + DB_NAME(db.resource_database_id) AS DatabaseName, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + s.open_tran AS OpenTransactionCount + FROM sys.sysprocesses s + INNER JOIN sys.dm_exec_connections c ON s.spid = c.session_id + INNER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.spid = db.request_session_id + WHERE s.status = 'sleeping' + AND s.open_tran > 0 + AND s.last_batch < DATEADD(ss, -10, SYSDATETIME()) + AND EXISTS(SELECT * FROM sys.dm_tran_locks WHERE request_session_id = s.spid + AND NOT (resource_type = N'DATABASE' AND request_mode = N'S' AND request_status = N'GRANT' AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE')); + END + /*Query Problems - Clients using implicit transactions - CheckID 37 */ + IF @Seconds > 0 + AND ( @@VERSION NOT LIKE 'Microsoft SQL Server 2005%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008%' + AND @@VERSION NOT LIKE 'Microsoft SQL Server 2008 R2%' ) + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 37',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N'INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) + SELECT 37 AS CheckId, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Implicit Transactions'', + ''https://www.brentozar.com/go/ImplicitTransactions/'' AS URL, + ''Database: '' + DB_NAME(s.database_id) + '' '' + CHAR(13) + CHAR(10) + + ''Host: '' + s.[host_name] + '' '' + CHAR(13) + CHAR(10) + + ''Program: '' + s.[program_name] + '' '' + CHAR(13) + CHAR(10) + + CONVERT(NVARCHAR(10), s.open_transaction_count) + + '' open transactions since: '' + + CONVERT(NVARCHAR(30), tat.transaction_begin_time) + ''. '' + AS Details, + ''Run sp_BlitzWho and check the is_implicit_transaction column to spot the culprits. +If one of them is a lead blocker, consider killing that query.'' AS HowToStopit, + tat.transaction_begin_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + s.database_id, + DB_NAME(s.database_id) AS DatabaseName, + NULL AS Querytext, + s.open_transaction_count AS OpenTransactionCount + FROM sys.dm_tran_active_transactions AS tat + LEFT JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + LEFT JOIN sys.dm_exec_sessions AS s + ON s.session_id = tst.session_id + WHERE tat.name = ''implicit_transaction''; + ' + EXECUTE sp_executesql @StringToExecute; + END; /* Query Problems - Query Rolling Back - CheckID 9 */ IF @Seconds > 0 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText) - SELECT 9 AS CheckID, - 1 AS Priority, - 'Query Problems' AS FindingGroup, - 'Query Rolling Back' AS Finding, - 'http://www.BrentOzar.com/askbrent/rollback/' AS URL, - 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, - 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, - r.start_time AS StartTime, - s.login_name AS LoginName, - 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, - (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText - FROM sys.dm_exec_sessions s - INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id - INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id - LEFT OUTER JOIN ( - SELECT DISTINCT request_session_id, resource_database_id - FROM sys.dm_tran_locks - WHERE resource_type = N'DATABASE' - AND request_mode = N'S' - AND request_status = N'GRANT' - AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id - WHERE r.status = 'rollback' - - - /* Server Performance - Page Life Expectancy Low - CheckID 10 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 10 AS CheckID, - 50 AS Priority, - 'Server Performance' AS FindingGroup, - 'Page Life Expectancy Low' AS Finding, - 'http://www.BrentOzar.com/askbrent/page-life-expectancy/' AS URL, - 'SQL Server Buffer Manager:Page life expectancy is ' + CAST(c.cntr_value AS NVARCHAR(10)) + ' seconds.' + @LineFeed - + 'This means SQL Server can only keep data pages in memory for that many seconds after reading those pages in from storage.' + @LineFeed - + 'This is a symptom, not a cause - it indicates very read-intensive queries that need an index, or insufficient server memory.' AS Details, - 'Add more memory to the server, or find the queries reading a lot of data, and make them more efficient (or fix them with indexes).' AS HowToStopIt - FROM sys.dm_os_performance_counters c - WHERE object_name LIKE 'SQLServer:Buffer Manager%' - AND counter_name LIKE 'Page life expectancy%' - AND cntr_value < 300 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 9',10,1) WITH NOWAIT; + END + + IF EXISTS (SELECT * FROM sys.dm_exec_requests WHERE total_elapsed_time > 5000 AND request_id > 0) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, QueryHash) + SELECT 9 AS CheckID, + 1 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query Rolling Back' AS Finding, + 'https://www.brentozar.com/askbrent/rollback/' AS URL, + 'Rollback started at ' + CAST(r.start_time AS NVARCHAR(100)) + ', is ' + CAST(r.percent_complete AS NVARCHAR(100)) + '% complete.' AS Details, + 'Unfortunately, you can''t stop this. Whatever you do, don''t restart the server in an attempt to fix it - SQL Server will keep rolling back.' AS HowToStopIt, + r.start_time AS StartTime, + s.login_name AS LoginName, + 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, + (SELECT TOP 1 [text] FROM sys.dm_exec_sql_text(c.most_recent_sql_handle)) AS QueryText, + r.query_hash + FROM sys.dm_exec_sessions s + INNER JOIN sys.dm_exec_connections c ON s.session_id = c.session_id + INNER JOIN sys.dm_exec_requests r ON s.session_id = r.session_id + LEFT OUTER JOIN ( + SELECT DISTINCT request_session_id, resource_database_id + FROM sys.dm_tran_locks + WHERE resource_type = N'DATABASE' + AND request_mode = N'S' + AND request_status = N'GRANT' + AND request_owner_type = N'SHARED_TRANSACTION_WORKSPACE') AS db ON s.session_id = db.request_session_id + WHERE r.status = 'rollback'; + END + + IF @Seconds > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 1 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END /* Server Performance - Too Much Free Memory - CheckID 34 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 34',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 34 AS CheckID, 50 AS Priority, 'Server Performance' AS FindingGroup, 'Too Much Free Memory' AS Finding, - 'https://BrentOzar.com/go/freememory' AS URL, + 'https://www.brentozar.com/go/freememory' AS URL, CAST((CAST(cFree.cntr_value AS BIGINT) / 1024 / 1024 ) AS NVARCHAR(100)) + N'GB of free memory inside SQL Server''s buffer pool,' + @LineFeed + ' which is ' + CAST((CAST(cTotal.cntr_value AS BIGINT) / 1024 / 1024) AS NVARCHAR(100)) + N'GB. You would think lots of free memory would be good, but check out the URL for more information.' AS Details, 'Run sp_BlitzCache @SortOrder = ''memory grant'' to find queries with huge memory grants and tune them.' AS HowToStopIt FROM sys.dm_os_performance_counters cFree @@ -1549,29 +1972,42 @@ BEGIN AND cTotal.counter_name = N'Total Server Memory (KB) ' WHERE cFree.object_name LIKE N'%Memory Manager%' AND cFree.counter_name = N'Free Memory (KB) ' - AND CAST(cTotal.cntr_value AS BIGINT) > 20480000000 + AND CAST(cFree.cntr_value AS BIGINT) > 20480000000 AND CAST(cTotal.cntr_value AS BIGINT) * .3 <= CAST(cFree.cntr_value AS BIGINT) AND CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) NOT LIKE '%Standard%'; /* Server Performance - Target Memory Lower Than Max - CheckID 35 */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) - SELECT 35 AS CheckID, - 10 AS Priority, - 'Server Performance' AS FindingGroup, - 'Target Memory Lower Than Max' AS Finding, - 'https://BrentOzar.com/go/target' AS URL, - N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed - + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, - 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt - FROM sys.configurations cMax - INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' - AND cTarget.counter_name = N'Target Server Memory (KB) ' - WHERE cMax.name = 'max server memory (MB)' - AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) - AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ - AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 35',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 35 AS CheckID, + 10 AS Priority, + 'Server Performance' AS FindingGroup, + 'Target Memory Lower Than Max' AS Finding, + 'https://www.brentozar.com/go/target' AS URL, + N'Max server memory is ' + CAST(cMax.value_in_use AS NVARCHAR(50)) + N' MB but target server memory is only ' + CAST((CAST(cTarget.cntr_value AS BIGINT) / 1024) AS NVARCHAR(50)) + N' MB,' + @LineFeed + + N'indicating that SQL Server may be under external memory pressure or max server memory may be set too high.' AS Details, + 'Investigate what OS processes are using memory, and double-check the max server memory setting.' AS HowToStopIt + FROM sys.configurations cMax + INNER JOIN sys.dm_os_performance_counters cTarget ON cTarget.object_name LIKE N'%Memory Manager%' + AND cTarget.counter_name = N'Target Server Memory (KB) ' + WHERE cMax.name = 'max server memory (MB)' + AND CAST(cMax.value_in_use AS BIGINT) >= 1.5 * (CAST(cTarget.cntr_value AS BIGINT) / 1024) + AND CAST(cMax.value_in_use AS BIGINT) < 2147483647 /* Not set to default of unlimited */ + AND CAST(cTarget.cntr_value AS BIGINT) < .8 * (SELECT available_physical_memory_kb FROM sys.dm_os_sys_memory); /* Target memory less than 80% of physical memory (in case they set max too high) */ + END /* Server Info - Database Size, Total GB - CheckID 21 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 21',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) SELECT 21 AS CheckID, 251 AS Priority, @@ -1579,11 +2015,16 @@ BEGIN 'Database Size, Total GB' AS Finding, CAST(SUM (CAST(size AS BIGINT)*8./1024./1024.) AS VARCHAR(100)) AS Details, SUM (CAST(size AS BIGINT))*8./1024./1024. AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL + 'https://www.brentozar.com/askbrent/' AS URL FROM #MasterFiles - WHERE database_id > 4 + WHERE database_id > 4; /* Server Info - Database Count - CheckID 22 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 22',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) SELECT 22 AS CheckID, 251 AS Priority, @@ -1591,47 +2032,464 @@ BEGIN 'Database Count' AS Finding, CAST(SUM(1) AS VARCHAR(100)) AS Details, SUM (1) AS DetailsInt, - 'http://www.BrentOzar.com/askbrent/' AS URL + 'https://www.brentozar.com/askbrent/' AS URL FROM sys.databases - WHERE database_id > 4 + WHERE database_id > 4; + + + /* Server Info - Memory Grants pending - CheckID 39 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 39',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 39 AS CheckID, + 50 AS Priority, + 'Server Performance' AS FindingGroup, + 'Memory Grants Pending' AS Finding, + CAST(PendingGrants.Details AS NVARCHAR(50)) AS Details, + PendingGrants.DetailsInt, + 'https://www.brentozar.com/blitz/memory-grants/' AS URL + FROM + ( + SELECT + COUNT(1) AS Details, + COUNT(1) AS DetailsInt + FROM sys.dm_exec_query_memory_grants AS Grants + WHERE queue_id IS NOT NULL + ) AS PendingGrants + WHERE PendingGrants.Details > 0; + + /* Server Info - Memory Grant/Workspace info - CheckID 40 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 40',10,1) WITH NOWAIT; + END + + DECLARE @MaxWorkspace BIGINT + SET @MaxWorkspace = (SELECT CAST(cntr_value AS BIGINT)/1024 FROM #PerfmonStats WHERE counter_name = N'Maximum Workspace Memory (KB)') + + IF (@MaxWorkspace IS NULL + OR @MaxWorkspace = 0) + BEGIN + SET @MaxWorkspace = 1 + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) + SELECT 40 AS CheckID, + 251 AS Priority, + 'Server Info' AS FindingGroup, + 'Memory Grant/Workspace info' AS Finding, + + 'Grants Outstanding: ' + CAST((SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS NVARCHAR(50)) + @LineFeed + + 'Total Granted(MB): ' + CAST(ISNULL(SUM(Grants.granted_memory_kb) / 1024, 0) AS NVARCHAR(50)) + @LineFeed + + 'Total WorkSpace(MB): ' + CAST(ISNULL(@MaxWorkspace, 0) AS NVARCHAR(50)) + @LineFeed + + 'Granted workspace: ' + CAST(ISNULL((CAST(SUM(Grants.granted_memory_kb) / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + @LineFeed + + 'Oldest Grant in seconds: ' + CAST(ISNULL(DATEDIFF(SECOND, MIN(Grants.request_time), GETDATE()), 0) AS NVARCHAR(50)) AS Details, + (SELECT COUNT(*) FROM sys.dm_exec_query_memory_grants WHERE queue_id IS NULL) AS DetailsInt, + 'https://www.brentozar.com/askbrent/' AS URL + FROM sys.dm_exec_query_memory_grants AS Grants; + + /* Query Problems - Queries with high memory grants - CheckID 46 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 46',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL, QueryText, QueryPlan) + SELECT 46 AS CheckID, + 100 AS Priority, + 'Query Problems' AS FindingGroup, + 'Query with a memory grant exceeding ' + +CAST(@MemoryGrantThresholdPct AS NVARCHAR(15)) + +'%' AS Finding, + 'Granted size: '+ CAST(CAST(Grants.granted_memory_kb / 1024 AS INT) AS NVARCHAR(50)) + +N'MB ' + + @LineFeed + +N'Granted pct of max workspace: ' + + CAST(ISNULL((CAST(Grants.granted_memory_kb / 1024 AS MONEY) + / CAST(@MaxWorkspace AS MONEY)) * 100, 0) AS NVARCHAR(50)) + '%' + + @LineFeed + +N'SQLHandle: ' + +CONVERT(NVARCHAR(128),Grants.[sql_handle],1), + 'https://www.brentozar.com/memory-grants-sql-servers-public-toilet/' AS URL, + SQLText.[text], + QueryPlan.query_plan + FROM sys.dm_exec_query_memory_grants AS Grants + OUTER APPLY sys.dm_exec_sql_text(Grants.[sql_handle]) AS SQLText + OUTER APPLY sys.dm_exec_query_plan(Grants.[plan_handle]) AS QueryPlan + WHERE Grants.granted_memory_kb > ((@MemoryGrantThresholdPct/100.00)*(@MaxWorkspace*1024)); + + /* Query Problems - Memory Leak in USERSTORE_TOKENPERM Cache - CheckID 45 */ + IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_os_memory_clerks') AND name = 'pages_kb') + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45',10,1) WITH NOWAIT; + END + + /* SQL 2012+ version */ + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Query Problems'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN pages_kb * 1.0 ELSE 0.0 END) / SUM(pages_kb) >= 0.1 + AND SUM(pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + ELSE + BEGIN + /* Antiques Roadshow SQL 2008R2 - version */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 45 (Legacy version)',10,1) WITH NOWAIT; + END + + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, URL) + SELECT 45 AS CheckID, + 50 AS Priority, + ''Performance'' AS FindingsGroup, + ''Memory Leak in USERSTORE_TOKENPERM Cache'' AS Finding, + N''UserStore_TokenPerm clerk is using '' + CAST(CAST(SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + + N''GB RAM, total buffer pool is '' + CAST(CAST(SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 AS INT) AS NVARCHAR(100)) + N''GB.'' + AS details, + ''https://www.BrentOzar.com/go/userstore'' AS URL + FROM sys.dm_os_memory_clerks + HAVING SUM(CASE WHEN type = ''USERSTORE_TOKENPERM'' AND name = ''TokenAndPermUserStore'' THEN single_pages_kb + multi_pages_kb * 1.0 ELSE 0.0 END) / SUM(single_pages_kb + multi_pages_kb) >= 0.1 + AND SUM(single_pages_kb + multi_pages_kb) / 1024.0 / 1024.0 >= 1; /* At least 1GB RAM overall */'; + EXEC sp_executesql @StringToExecute; + END + + + + IF @Seconds > 0 + BEGIN + + IF EXISTS ( SELECT 1/0 + FROM sys.all_objects AS ao + WHERE ao.name = 'dm_exec_query_profiles' ) + BEGIN + + IF EXISTS( SELECT 1/0 + FROM sys.dm_exec_requests AS r + JOIN sys.dm_exec_sessions AS s + ON r.session_id = s.session_id + WHERE s.host_name IS NOT NULL + AND r.total_elapsed_time > 5000 + AND r.request_id > 0 ) + BEGIN + + SET @StringToExecute = N' + DECLARE @bad_estimate TABLE + ( + session_id INT, + request_id INT, + estimate_inaccuracy BIT + ); + + INSERT @bad_estimate ( session_id, request_id, estimate_inaccuracy ) + SELECT x.session_id, + x.request_id, + x.estimate_inaccuracy + FROM ( + SELECT deqp.session_id, + deqp.request_id, + CASE WHEN (deqp.row_count/10000) > deqp.estimate_row_count + THEN 1 + ELSE 0 + END AS estimate_inaccuracy + FROM sys.dm_exec_query_profiles AS deqp + INNER JOIN sys.dm_exec_requests r ON deqp.session_id = r.session_id AND deqp.request_id = r.request_id + WHERE deqp.session_id <> @@SPID + AND r.total_elapsed_time > 5000 + ) AS x + WHERE x.estimate_inaccuracy = 1 + GROUP BY x.session_id, + x.request_id, + x.estimate_inaccuracy; + + DECLARE @parallelism_skew TABLE + ( + session_id INT, + request_id INT, + parallelism_skew BIT + ); + + INSERT @parallelism_skew ( session_id, request_id, parallelism_skew ) + SELECT y.session_id, + y.request_id, + y.parallelism_skew + FROM ( + SELECT x.session_id, + x.request_id, + x.node_id, + x.thread_id, + x.row_count, + x.sum_node_rows, + x.node_dop, + x.sum_node_rows / x.node_dop AS even_distribution, + x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) AS skew_percent, + CASE + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) > 2. + THEN 1 + WHEN x.row_count > 10000 + AND x.row_count / (1. * ISNULL(NULLIF(x.sum_node_rows / x.node_dop, 0), 1)) < 0.5 + THEN 1 + ELSE 0 + END AS parallelism_skew + FROM ( + SELECT deqp.session_id, + deqp.request_id, + deqp.node_id, + deqp.thread_id, + deqp.row_count, + SUM(deqp.row_count) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS sum_node_rows, + COUNT(*) + OVER ( PARTITION BY deqp.session_id, + deqp.request_id, + deqp.node_id + ORDER BY deqp.row_count + ROWS BETWEEN UNBOUNDED PRECEDING + AND UNBOUNDED FOLLOWING ) + AS node_dop + FROM sys.dm_exec_query_profiles AS deqp + WHERE deqp.thread_id > 0 + AND deqp.session_id <> @@SPID + AND EXISTS + ( + SELECT 1/0 + FROM sys.dm_exec_query_profiles AS deqp2 + WHERE deqp.session_id = deqp2.session_id + AND deqp.node_id = deqp2.node_id + AND deqp2.thread_id > 0 + GROUP BY deqp2.session_id, deqp2.node_id + HAVING COUNT(deqp2.node_id) > 1 + ) + ) AS x + ) AS y + WHERE y.parallelism_skew = 1 + GROUP BY y.session_id, + y.request_id, + y.parallelism_skew; + + /* Queries in dm_exec_query_profiles showing signs of poor cardinality estimates - CheckID 42 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 42'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 42 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x cardinality misestimations'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(b.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a large cardinality misestimate'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @bad_estimate AS b + JOIN sys.dm_exec_requests AS r + ON r.session_id = b.session_id + AND r.request_id = b.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = b.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + /* GitHub #3210 */ + SET @StringToExecute = N' + SET LOCK_TIMEOUT 1000 ' + @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + SET @StringToExecute = @StringToExecute + N'; + + /* Queries in dm_exec_query_profiles showing signs of unbalanced parallelism - CheckID 43 */ + IF (@Debug = 1) + BEGIN + RAISERROR(''Running CheckID 43'',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults + (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount, QueryHash, QueryPlan) + SELECT 43 AS CheckID, + 100 AS Priority, + ''Query Performance'' AS FindingsGroup, + ''Queries with 10000x skewed parallelism'' AS Findings, + ''https://www.brentozar.com/go/skewedup'' AS URL, + ''The query on SPID '' + + RTRIM(p.session_id) + + '' has been running for '' + + RTRIM(r.total_elapsed_time / 1000) + + '' seconds, with a parallel threads doing uneven work.'' AS Details, + ''No quick fix here: time to dig into the actual execution plan. '' AS HowToStopIt, + r.start_time, + s.login_name, + s.nt_user_name, + s.program_name, + s.host_name, + r.database_id, + DB_NAME(r.database_id), + dest.text, + s.open_transaction_count, + r.query_hash, '; + + IF @dm_exec_query_statistics_xml = 1 + SET @StringToExecute = @StringToExecute + N' COALESCE(qs_live.query_plan, qp.query_plan) AS query_plan '; + ELSE + SET @StringToExecute = @StringToExecute + N' qp.query_plan '; + + SET @StringToExecute = @StringToExecute + N' + FROM @parallelism_skew AS p + JOIN sys.dm_exec_requests AS r + ON r.session_id = p.session_id + AND r.request_id = p.request_id + JOIN sys.dm_exec_sessions AS s + ON s.session_id = p.session_id + CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) AS dest + CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) AS qp '; + + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_exec_query_statistics_xml') + SET @StringToExecute = @StringToExecute + N' OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live '; + + + SET @StringToExecute = @StringToExecute + N';'; + + EXECUTE sp_executesql @StringToExecute, N'@Debug BIT',@Debug = @Debug; + END + + END + END - /* Server Performance - High CPU Utilization CheckID 24 */ + /* Server Performance - High CPU Utilization - CheckID 24 */ IF @Seconds < 30 BEGIN /* If we're waiting less than 30 seconds, run this check now rather than wait til the end. We get this data from the ring buffers, and it's only updated once per minute, so might as well get it now - whereas if we're checking 30+ seconds, it might get updated by the end of our sp_BlitzFirst session. */ - 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, 'http://www.BrentOzar.com/go/cpu' - FROM ( - SELECT record, - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle - 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 + IF (@Debug = 1) + BEGIN + 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 23, 250, 'Server Info', 'CPU Utilization', CAST(100 - SystemIdle AS NVARCHAR(20)) + N'%. Ring buffer details: ' + CAST(record AS NVARCHAR(4000)), 100 - SystemIdle, 'http://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 + ) AS ShreddedCpuXml + ) AS OsCpu + WHERE CpuUsage >= 50; + + /* CPU Utilization - CheckID 23 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + END + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + WITH y + AS + ( + /* 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 + FROM + ( SELECT CONVERT(XML, dorb.record) AS record, + DATEADD(ms, -( ts.ms_ticks - dorb.timestamp ), GETDATE()) AS event_date + FROM sys.dm_os_ring_buffers AS dorb + CROSS JOIN + ( SELECT dosi.ms_ticks FROM sys.dm_os_sys_info AS dosi ) AS ts + 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 + 23, + 250, + 'Server Info', + 'CPU Utilization', + 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.cpu_usage + + '% ON ' + + y2.event_date + + ' Ring buffer details: ' + + y2.record + FROM y AS y2 + ORDER BY y2.event_date_raw DESC + FOR XML PATH(N''), TYPE ).value(N'.[1]', N'VARCHAR(MAX)'), 1, 1, N'') AS query + FROM y + ORDER BY y.event_date_raw DESC; + - /* Highlight if non SQL processes are using >25% CPU */ + /* Highlight if non SQL processes are using >25% CPU - CheckID 28 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 28',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, Details, DetailsInt, URL) - SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'http://www.BrentOzar.com/go/cpu' + SELECT 28, 50, 'Server Performance', 'High CPU Utilization - Not SQL', CONVERT(NVARCHAR(100),100 - (y.SQLUsage + y.SystemIdle)) + N'% - Other Processes (not SQL Server) are using this much CPU. This may impact on the performance of your SQL Server instance', 100 - (y.SQLUsage + y.SystemIdle), 'https://www.brentozar.com/go/cpu' FROM ( SELECT record, record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') AS SystemIdle @@ -1644,98 +2502,290 @@ BEGIN ORDER BY timestamp DESC) AS rb ) AS y 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 */ - - RAISERROR('Finished running investigatory queries',10,1) WITH NOWAIT; + END; /* IF @Seconds < 30 */ + /* Query Problems - Statistics Updated Recently - CheckID 44 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 44',10,1) WITH NOWAIT; + END - /* End of checks. If we haven't waited @Seconds seconds, wait. */ - IF SYSDATETIMEOFFSET() < @FinishSampleTime + IF 20 >= (SELECT COUNT(*) FROM sys.databases WHERE name NOT IN ('master', 'model', 'msdb', 'tempdb')) + AND @Seconds > 0 + BEGIN + CREATE TABLE #UpdatedStats (HowToStopIt NVARCHAR(4000), RowsForSorting BIGINT); + IF EXISTS(SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_properties') BEGIN - RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; - WAITFOR TIME @FinishSampleTimeWaitFor; - END + /* We don't want to hang around to obtain locks */ + SET LOCK_TIMEOUT 0; - RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + SET @StringToExecute = N'USE [?];' + @LineFeed; + ELSE + SET @StringToExecute = N''; + + SET @StringToExecute = @StringToExecute + 'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SET LOCK_TIMEOUT 1000;' + @LineFeed + + 'BEGIN TRY' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) + N''.'' +' + @LineFeed + + ' QUOTENAME(SCHEMA_NAME(obj.schema_id)) + N''.'' +' + @LineFeed + + ' QUOTENAME(obj.name) +' + @LineFeed + + ' N'' statistic '' + QUOTENAME(stat.name) + ' + @LineFeed + + ' N'' was updated on '' + CONVERT(NVARCHAR(50), sp.last_updated, 121) + N'','' + ' + @LineFeed + + ' N'' had '' + CAST(sp.rows AS NVARCHAR(50)) + N'' rows, with '' +' + @LineFeed + + ' CAST(sp.rows_sampled AS NVARCHAR(50)) + N'' rows sampled,'' + ' + @LineFeed + + ' N'' producing '' + CAST(sp.steps AS NVARCHAR(50)) + N'' steps in the histogram.'',' + @LineFeed + + ' sp.rows' + @LineFeed + + ' FROM sys.objects AS obj WITH (NOLOCK)' + @LineFeed + + ' INNER JOIN sys.stats AS stat WITH (NOLOCK) ON stat.object_id = obj.object_id ' + @LineFeed + + ' CROSS APPLY sys.dm_db_stats_properties(stat.object_id, stat.stats_id) AS sp ' + @LineFeed + + ' WHERE sp.last_updated > DATEADD(MI, -15, GETDATE())' + @LineFeed + + ' AND obj.is_ms_shipped = 0' + @LineFeed + + ' AND ''[?]'' <> ''[tempdb]'';' + @LineFeed + + 'END TRY' + @LineFeed + + 'BEGIN CATCH' + @LineFeed + + ' IF (ERROR_NUMBER() = 1222)' + @LineFeed + + ' BEGIN ' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as the lock timeout was exceeded,''+' + @LineFeed + + ' N'' this is likely due to an Index operation in Progress'',' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + ' ELSE' + @LineFeed + + ' BEGIN' + @LineFeed + + ' INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting)' + @LineFeed + + ' SELECT HowToStopIt = ' + @LineFeed + + ' QUOTENAME(DB_NAME()) +' + @LineFeed + + ' N'' No information could be retrieved as a result of error: ''+' + @LineFeed + + ' CAST(ERROR_NUMBER() AS NVARCHAR(10)) +' + @LineFeed + + ' N'' with message: ''+' + @LineFeed + + ' CAST(ERROR_MESSAGE() AS NVARCHAR(128)),' + @LineFeed + + ' -1' + @LineFeed + + ' END' + @LineFeed + + 'END CATCH' + ; + + IF SERVERPROPERTY('EngineEdition') <> 5 /*SERVERPROPERTY('Edition') <> 'SQL Azure'*/ + BEGIN + BEGIN TRY + EXEC sp_MSforeachdb @StringToExecute; + END TRY + BEGIN CATCH + IF (ERROR_NUMBER() = 1222) + BEGIN + INSERT INTO #UpdatedStats(HowToStopIt, RowsForSorting) + SELECT HowToStopIt = N'No information could be retrieved as the lock timeout was exceeded while iterating databases,' + + N' this is likely due to an Index operation in Progress', -1; + END + ELSE + BEGIN; + THROW; + END + END CATCH + END + ELSE + EXEC(@StringToExecute); + + /* Set timeout back to a default value of -1 */ + SET LOCK_TIMEOUT -1; + END; + + /* We mark timeout exceeded with a -1 so only show these IF there is statistics info that succeeded */ + IF EXISTS (SELECT * FROM #UpdatedStats WHERE RowsForSorting > -1) + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 44 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingGroup, + 'Statistics Updated Recently' AS Finding, + 'https://www.brentozar.com/go/stats' AS URL, + 'In the last 15 minutes, statistics were updated. To see which ones, click the HowToStopIt column.' + @LineFeed + @LineFeed + + 'This effectively clears the plan cache for queries that involve these tables,' + @LineFeed + + 'which thereby causes parameter sniffing: those queries are now getting brand new' + @LineFeed + + 'query plans based on whatever parameters happen to call them next.' + @LineFeed + @LineFeed + + 'Be on the lookout for sudden parameter sniffing issues after this time range.', + HowToStopIt = (SELECT (SELECT HowToStopIt + NCHAR(10)) + FROM #UpdatedStats + ORDER BY RowsForSorting DESC + FOR XML PATH('')); + + 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 + RAISERROR('Running CheckID 49',10,1) WITH NOWAIT; + END + IF CAST(SERVERPROPERTY('edition') AS VARCHAR(100)) LIKE '%64%' AND SERVERPROPERTY('EngineEdition') <> 5 + BEGIN + IF @logical_processors <= 4 + SET @max_worker_threads = 512; + ELSE IF @logical_processors > 64 AND + ((@v = 13 AND @build >= 5026) OR @v >= 14) + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 32) + ELSE + SET @max_worker_threads = 512 + ((@logical_processors - 4) * 16) + + IF @max_worker_threads > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT 49 AS CheckID, + 210 AS Priority, + 'Potential Upcoming Problems' AS FindingGroup, + 'High Number of Connections' AS Finding, + 'https://www.brentozar.com/archive/2014/05/connections-slow-sql-server-threadpool/' AS URL, + 'There are ' + CAST(SUM(1) AS VARCHAR(20)) + ' open connections, which would lead to ' + @LineFeed + 'worker thread exhaustion and THREADPOOL waits' + @LineFeed + 'if they all ran queries at the same time.' AS Details + FROM sys.dm_exec_connections c + HAVING SUM(1) > @max_worker_threads; + 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; + + + /* End of checks. If we haven't waited @Seconds seconds, wait. */ + IF DATEADD(SECOND,1,SYSDATETIMEOFFSET()) < @FinishSampleTime + BEGIN + RAISERROR('Waiting to match @Seconds parameter',10,1) WITH NOWAIT; + WAITFOR TIME @FinishSampleTimeWaitFor; + END; + + IF @total_cpu_usage IN (0, 1) + BEGIN + EXEC sys.sp_executesql + @get_thread_time_ms, + N'@thread_time_ms FLOAT OUTPUT', + @thread_time_ms OUTPUT; + END + + RAISERROR('Capturing second pass of wait stats, perfmon counters, file stats',10,1) WITH NOWAIT; /* Populate #FileStats, #PerfmonStats, #WaitStats with DMV data. In a second, we'll compare these. */ - INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) - SELECT - x.Pass, - x.SampleTime, - x.wait_type, - SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, - SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, - SUM(x.sum_waiting_tasks) AS sum_waiting_tasks - FROM ( - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - owt.wait_type, - SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) - - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, - 0 AS sum_signal_wait_time_ms, - CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks - FROM sys.dm_os_waiting_tasks owt - WHERE owt.session_id > 50 - AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END - UNION ALL - SELECT - 2 AS Pass, - SYSDATETIMEOFFSET() AS SampleTime, - os.wait_type, - SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, - SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, - SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks - FROM sys.dm_os_wait_stats os + SET @StringToExecute = N' + INSERT #WaitStats(Pass, SampleTime, wait_type, wait_time_ms, thread_time_ms, signal_wait_time_ms, waiting_tasks_count) + SELECT + x.Pass, + x.SampleTime, + x.wait_type, + SUM(x.sum_wait_time_ms) AS sum_wait_time_ms, + @thread_time_ms AS thread_time_ms, + SUM(x.sum_signal_wait_time_ms) AS sum_signal_wait_time_ms, + SUM(x.sum_waiting_tasks) AS sum_waiting_tasks + FROM ( + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + owt.wait_type, + SUM(owt.wait_duration_ms) OVER (PARTITION BY owt.wait_type, owt.session_id) + - CASE WHEN @Seconds = 0 THEN 0 ELSE (@Seconds * 1000) END AS sum_wait_time_ms, + 0 AS sum_signal_wait_time_ms, + CASE @Seconds WHEN 0 THEN 0 ELSE 1 END AS sum_waiting_tasks + FROM sys.dm_os_waiting_tasks owt + WHERE owt.session_id > 50 + AND owt.wait_duration_ms >= CASE @Seconds WHEN 0 THEN 0 ELSE @Seconds * 1000 END + UNION ALL + SELECT + 2 AS Pass, + SYSDATETIMEOFFSET() AS SampleTime, + os.wait_type, + SUM(os.wait_time_ms) OVER (PARTITION BY os.wait_type) AS sum_wait_time_ms, + SUM(os.signal_wait_time_ms) OVER (PARTITION BY os.wait_type ) AS sum_signal_wait_time_ms, + SUM(os.waiting_tasks_count) OVER (PARTITION BY os.wait_type) AS sum_waiting_tasks '; + + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_db_wait_stats os '; + ELSE + SET @StringToExecute = @StringToExecute + N' FROM sys.dm_os_wait_stats os '; + + SET @StringToExecute = @StringToExecute + N' ) x - WHERE x.wait_type NOT IN ( - 'BROKER_EVENTHANDLER' - , 'BROKER_RECEIVE_WAITFOR' - , 'BROKER_TASK_STOP' - , 'BROKER_TO_FLUSH' - , 'BROKER_TRANSMITTER' - , 'CHECKPOINT_QUEUE' - , 'DBMIRROR_DBM_EVENT' - , 'DBMIRROR_DBM_MUTEX' - , 'DBMIRROR_EVENTS_QUEUE' - , 'DBMIRROR_WORKER_QUEUE' - , 'DBMIRRORING_CMD' - , 'DIRTY_PAGE_POLL' - , 'DISPATCHER_QUEUE_SEMAPHORE' - , 'FT_IFTS_SCHEDULER_IDLE_WAIT' - , 'FT_IFTSHC_MUTEX' - , 'HADR_CLUSAPI_CALL' - , 'HADR_FILESTREAM_IOMGR_IOCOMPLETION' - , 'HADR_LOGCAPTURE_WAIT' - , 'HADR_NOTIFICATION_DEQUEUE' - , 'HADR_TIMER_TASK' - , 'HADR_WORK_QUEUE' - , 'LAZYWRITER_SLEEP' - , 'LOGMGR_QUEUE' - , 'ONDEMAND_TASK_QUEUE' - , 'PREEMPTIVE_HADR_LEASE_MECHANISM' - , 'PREEMPTIVE_SP_SERVER_DIAGNOSTICS' - , 'QDS_ASYNC_QUEUE' - , 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP' - , 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP' - , 'QDS_SHUTDOWN_QUEUE' - , 'REDO_THREAD_PENDING_WORK' - , 'REQUEST_FOR_DEADLOCK_SEARCH' - , 'SLEEP_SYSTEMTASK' - , 'SLEEP_TASK' - , 'SP_SERVER_DIAGNOSTICS_SLEEP' - , 'SQLTRACE_BUFFER_FLUSH' - , 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP' - , 'UCS_SESSION_REGISTRATION' - , 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG' - , 'WAITFOR' - , 'XE_DISPATCHER_WAIT' - , 'XE_LIVE_TARGET_TVF' - , 'XE_TIMER_EVENT' + WHERE NOT EXISTS + ( + SELECT * + FROM ##WaitCategories AS wc + WHERE wc.WaitType = x.wait_type + AND wc.Ignorable = 1 ) GROUP BY x.Pass, x.SampleTime, x.wait_type - ORDER BY sum_wait_time_ms DESC; + ORDER BY sum_wait_time_ms DESC;'; + + EXEC sys.sp_executesql + @StringToExecute, + N'@StartSampleTime DATETIMEOFFSET, + @Seconds INT, + @thread_time_ms FLOAT', + @StartSampleTime, + @Seconds, + @thread_time_ms; + + WITH w AS + ( + SELECT + total_waits = + CONVERT + ( + FLOAT, + SUM(ws.wait_time_ms) + ) + FROM #WaitStats AS ws + WHERE Pass = 2 + ) + UPDATE ws + SET ws.thread_time_ms += w.total_waits + FROM #WaitStats AS ws + CROSS JOIN w + WHERE ws.Pass = 2 + OPTION(RECOMPILE); + INSERT INTO #FileStats (Pass, SampleTime, DatabaseID, FileID, DatabaseName, FileLogicalName, SizeOnDiskMB, io_stall_read_ms , num_of_reads, [bytes_read] , io_stall_write_ms,num_of_writes, [bytes_written], PhysicalName, TypeDesc, avg_stall_read_ms, avg_stall_write_ms) @@ -1769,20 +2819,20 @@ BEGIN FROM #PerfmonCounters counters INNER JOIN sys.dm_os_performance_counters dmv ON counters.counter_name COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.counter_name) COLLATE SQL_Latin1_General_CP1_CI_AS AND counters.[object_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[object_name]) COLLATE SQL_Latin1_General_CP1_CI_AS - AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS) + AND (counters.[instance_name] IS NULL OR counters.[instance_name] COLLATE SQL_Latin1_General_CP1_CI_AS = RTRIM(dmv.[instance_name]) COLLATE SQL_Latin1_General_CP1_CI_AS); /* Set the latencies and averages. We could do this with a CTE, but we're not ambitious today. */ UPDATE fNow SET avg_stall_read_ms = ((fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads)) FROM #FileStats fNow INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_reads > fBase.num_of_reads AND fNow.io_stall_read_ms > fBase.io_stall_read_ms - WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0 + WHERE (fNow.num_of_reads - fBase.num_of_reads) > 0; UPDATE fNow SET avg_stall_write_ms = ((fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes)) FROM #FileStats fNow INNER JOIN #FileStats fBase ON fNow.DatabaseID = fBase.DatabaseID AND fNow.FileID = fBase.FileID AND fNow.SampleTime > fBase.SampleTime AND fNow.num_of_writes > fBase.num_of_writes AND fNow.io_stall_write_ms > fBase.io_stall_write_ms - WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0 + WHERE (fNow.num_of_writes - fBase.num_of_writes) > 0; UPDATE pNow SET [value_delta] = pNow.cntr_value - pFirst.cntr_value, @@ -1793,15 +2843,19 @@ BEGIN WHERE DATEDIFF(ss, pFirst.SampleTime, pNow.SampleTime) > 0; - /* If we're within 10 seconds of our projected finish time, do the plan cache analysis. */ - IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIME()) > 10 AND @CheckProcedureCache = 1 + /* Query Stats - If we're within 10 seconds of our projected finish time, do the plan cache analysis. - CheckID 18 */ + IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 18',10,1) WITH NOWAIT; + END INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) - VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'http://www.BrentOzar.com/go/topqueries', - 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.') + VALUES (18, 210, 'Query Stats', 'Plan Cache Analysis Skipped', 'https://www.brentozar.com/go/topqueries', + 'Due to excessive load, the plan cache analysis was skipped. To override this, use @ExpertMode = 1.'); - END + END; ELSE IF @CheckProcedureCache = 1 BEGIN @@ -1817,7 +2871,7 @@ BEGIN SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, NULL AS query_hash, NULL AS query_plan_hash, 0 FROM sys.dm_exec_query_stats qs WHERE qs.last_execution_time >= @StartSampleTimeText;'; - END + END; ELSE BEGIN SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) @@ -1827,8 +2881,8 @@ BEGIN INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID WHERE qs.last_execution_time >= @StartSampleTimeText AND attr.attribute = ''dbid'';'; - END - END + END; + END; ELSE BEGIN IF @FilterPlansByDatabase IS NULL @@ -1837,7 +2891,7 @@ BEGIN SELECT [sql_handle], 2 AS Pass, SYSDATETIMEOFFSET(), statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, 0 FROM sys.dm_exec_query_stats qs WHERE qs.last_execution_time >= @StartSampleTimeText'; - END + END; ELSE BEGIN SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) @@ -1847,8 +2901,8 @@ BEGIN INNER JOIN #FilterPlansByDatabase dbs ON CAST(attr.value AS INT) = dbs.DatabaseID WHERE qs.last_execution_time >= @StartSampleTimeText AND attr.attribute = ''dbid'';'; - END - END + END; + END; /* Old version pre-2016/06/13: IF @@VERSION LIKE 'Microsoft SQL Server 2005%' SET @StringToExecute = N'INSERT INTO #QueryStats ([sql_handle], Pass, SampleTime, statement_start_offset, statement_end_offset, plan_generation_num, plan_handle, execution_count, total_worker_time, total_physical_reads, total_logical_writes, total_logical_reads, total_clr_time, total_elapsed_time, creation_time, query_hash, query_plan_hash, Points) @@ -1934,9 +2988,14 @@ BEGIN FROM #QueryStats qs INNER JOIN qsTop ON qs.ID = qsTop.ID; - /* Query Stats - CheckID 17 - Most Resource-Intensive Queries */ - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle) - SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'http://www.BrentOzar.com/go/topqueries', + /* Query Stats - Most Resource-Intensive Queries - CheckID 17 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 17',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, QueryStatsNowID, QueryStatsFirstID, PlanHandle, QueryHash) + SELECT 17, 210, 'Query Stats', 'Most Resource-Intensive Queries', 'https://www.brentozar.com/go/topqueries', 'Query stats during the sample:' + @LineFeed + 'Executions: ' + CAST(qsNow.execution_count - (COALESCE(qsFirst.execution_count, 0)) AS NVARCHAR(100)) + @LineFeed + 'Elapsed Time: ' + CAST(qsNow.total_elapsed_time - (COALESCE(qsFirst.total_elapsed_time, 0)) AS NVARCHAR(100)) + @LineFeed + @@ -1970,36 +3029,42 @@ BEGIN END - qsNow.statement_start_offset) / 2) + 1), qsNow.ID AS QueryStatsNowID, qsFirst.ID AS QueryStatsFirstID, - qsNow.plan_handle AS PlanHandle + qsNow.plan_handle AS PlanHandle, + qsNow.query_hash FROM #QueryStats qsNow INNER JOIN #QueryStats qsTotal ON qsTotal.Pass = 0 LEFT OUTER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 CROSS APPLY sys.dm_exec_sql_text(qsNow.sql_handle) AS st CROSS APPLY sys.dm_exec_query_plan(qsNow.plan_handle) AS qp - WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL + WHERE qsNow.Points > 0 AND st.text IS NOT NULL AND qp.query_plan IS NOT NULL; UPDATE #BlitzFirstResults SET DatabaseID = CAST(attr.value AS INT), DatabaseName = DB_NAME(CAST(attr.value AS INT)) FROM #BlitzFirstResults CROSS APPLY sys.dm_exec_plan_attributes(#BlitzFirstResults.PlanHandle) AS attr - WHERE attr.attribute = 'dbid' + WHERE attr.attribute = 'dbid'; - END /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ + END; /* IF DATEDIFF(ss, @FinishSampleTime, SYSDATETIMEOFFSET()) > 10 AND @CheckProcedureCache = 1 */ RAISERROR('Analyzing changes between first and second passes of DMVs',10,1) WITH NOWAIT; /* Wait Stats - CheckID 6 */ /* Compare the current wait stats to the sample we took at the start, and insert the top 10 waits. */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 6',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) SELECT TOP 10 6 AS CheckID, 200 AS Priority, 'Wait Stats' AS FindingGroup, wNow.wait_type AS Finding, /* IF YOU CHANGE THIS, STUFF WILL BREAK. Other checks look for wait type names in the Finding field. See checks 11, 12 as example. */ - N'http://www.brentozar.com/sql/wait-stats/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + N'https://www.sqlskills.com/help/waits/' + LOWER(wNow.wait_type) + '/' AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt FROM #WaitStats wNow @@ -2008,29 +3073,40 @@ BEGIN ORDER BY (wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) DESC; /* Server Performance - Poison Wait Detected - CheckID 30 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 30',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DetailsInt) SELECT 30 AS CheckID, 10 AS Priority, 'Server Performance' AS FindingGroup, 'Poison Wait Detected: ' + wNow.wait_type AS Finding, - N'http://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, - 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(@Seconds AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, + N'https://www.brentozar.com/go/poison/#' + wNow.wait_type AS URL, + 'For ' + CAST(((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS NVARCHAR(100)) + ' seconds over the last ' + CASE @Seconds WHEN 0 THEN (CAST(DATEDIFF(dd,@StartSampleTime,@FinishSampleTime) AS NVARCHAR(10)) + ' days') ELSE (CAST(DATEDIFF(ss, wBase.SampleTime, wNow.SampleTime) AS NVARCHAR(10)) + ' seconds') END + ', SQL Server was waiting on this particular bottleneck.' + @LineFeed + @LineFeed AS Details, 'See the URL for more details on how to mitigate this wait type.' AS HowToStopIt, ((wNow.wait_time_ms - COALESCE(wBase.wait_time_ms,0)) / 1000) AS DetailsInt FROM #WaitStats wNow LEFT OUTER JOIN #WaitStats wBase ON wNow.wait_type = wBase.wait_type AND wNow.SampleTime > wBase.SampleTime - WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') AND wNow.wait_time_ms > wBase.wait_time_ms; + WHERE wNow.wait_type IN ('IO_QUEUE_LIMIT', 'IO_RETRY', 'LOG_RATE_GOVERNOR', 'POOL_LOG_RATE_GOVERNOR', 'PREEMPTIVE_DEBUG', 'RESMGR_THROTTLED', 'RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE','SE_REPL_CATCHUP_THROTTLE','SE_REPL_COMMIT_ACK','SE_REPL_COMMIT_TURN','SE_REPL_ROLLBACK_ACK','SE_REPL_SLOW_SECONDARY_THROTTLE','THREADPOOL') + AND wNow.wait_time_ms > (wBase.wait_time_ms + 1000); /* Server Performance - Slow Data File Reads - CheckID 11 */ IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'PAGEIOLATCH%') BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 11',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) SELECT TOP 10 11 AS CheckID, 50 AS Priority, 'Server Performance' AS FindingGroup, 'Slow Data File Reads' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, 'Your server is experiencing PAGEIOLATCH% waits due to slow data file reads. This file is one of the reasons why.' + @LineFeed + 'File: ' + fNow.PhysicalName + @LineFeed + 'Number of reads during the sample: ' + CAST((fNow.num_of_reads - fBase.num_of_reads) AS NVARCHAR(20)) + @LineFeed @@ -2045,17 +3121,22 @@ BEGIN WHERE (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) >= @FileLatencyThresholdMS AND fNow.TypeDesc = 'ROWS' ORDER BY (fNow.io_stall_read_ms - fBase.io_stall_read_ms) / (fNow.num_of_reads - fBase.num_of_reads) DESC; - END + END; /* Server Performance - Slow Log File Writes - CheckID 12 */ IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE Finding LIKE 'WRITELOG%') BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 12',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, DatabaseID, DatabaseName) SELECT TOP 10 12 AS CheckID, 50 AS Priority, 'Server Performance' AS FindingGroup, 'Slow Log File Writes' AS Finding, - 'http://www.BrentOzar.com/go/slow/' AS URL, + 'https://www.brentozar.com/blitz/slow-storage-reads-writes/' AS URL, 'Your server is experiencing WRITELOG waits due to slow log file writes. This file is one of the reasons why.' + @LineFeed + 'File: ' + fNow.PhysicalName + @LineFeed + 'Number of writes during the sample: ' + CAST((fNow.num_of_writes - fBase.num_of_writes) AS NVARCHAR(20)) + @LineFeed @@ -2070,16 +3151,43 @@ BEGIN WHERE (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) >= @FileLatencyThresholdMS AND fNow.TypeDesc = 'LOG' ORDER BY (fNow.io_stall_write_ms - fBase.io_stall_write_ms) / (fNow.num_of_writes - fBase.num_of_writes) DESC; + 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 + RAISERROR('Running CheckID 13',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 13 AS CheckID, 1 AS Priority, 'SQL Server Internal Maintenance' AS FindingGroup, 'Log File Growing' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-growing/' AS URL, + 'https://www.brentozar.com/askbrent/file-growing/' AS URL, 'Number of growths 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, 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt @@ -2087,16 +3195,21 @@ BEGIN WHERE ps.Pass = 2 AND object_name = @ServiceName + ':Databases' AND counter_name = 'Log Growths' - AND value_delta > 0 + AND value_delta > 0; /* SQL Server Internal Maintenance - Log File Shrinking - CheckID 14 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 14',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 14 AS CheckID, 1 AS Priority, 'SQL Server Internal Maintenance' AS FindingGroup, 'Log File Shrinking' AS Finding, - 'http://www.BrentOzar.com/askbrent/file-shrinking/' AS URL, + 'https://www.brentozar.com/askbrent/file-shrinking/' AS URL, 'Number of shrinks 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, 'Pre-grow data and log files during maintenance windows so that they do not grow during production loads. See the URL for more details.' AS HowToStopIt @@ -2104,15 +3217,20 @@ BEGIN WHERE ps.Pass = 2 AND object_name = @ServiceName + ':Databases' AND counter_name = 'Log Shrinks' - AND value_delta > 0 + AND value_delta > 0; /* Query Problems - Compilations/Sec High - CheckID 15 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 15',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 15 AS CheckID, 50 AS Priority, 'Query Problems' AS FindingGroup, 'Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/compilations/' AS URL, + 'https://www.brentozar.com/askbrent/compilations/' AS URL, 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + 'Number of compilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + 'For OLTP environments, Microsoft recommends that 90% of batch requests should hit the plan cache, and not be compiled from scratch. We are exceeding that threshold.' + @LineFeed AS Details, @@ -2124,16 +3242,22 @@ BEGIN WHERE ps.Pass = 2 AND ps.object_name = @ServiceName + ':SQL Statistics' AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Compilations are more than 10% of batch requests per second */ + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 compilations per second, or more compilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Compilations are more than 10% of batch requests per second */ /* Query Problems - Re-Compilations/Sec High - CheckID 16 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 16',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 16 AS CheckID, 50 AS Priority, 'Query Problems' AS FindingGroup, 'Re-Compilations/Sec High' AS Finding, - 'http://www.BrentOzar.com/askbrent/recompilations/' AS URL, + 'https://www.brentozar.com/askbrent/recompilations/' AS URL, 'Number of batch requests during the sample: ' + CAST(ps.value_delta AS NVARCHAR(20)) + @LineFeed + 'Number of recompilations during the sample: ' + CAST(psComp.value_delta AS NVARCHAR(20)) + @LineFeed + 'More than 10% of our queries are being recompiled. This is typically due to statistics changing on objects.' + @LineFeed AS Details, @@ -2145,17 +3269,23 @@ BEGIN WHERE ps.Pass = 2 AND ps.object_name = @ServiceName + ':SQL Statistics' AND ps.counter_name = 'Batch Requests/sec' - AND ps.value_delta > (1000 * @Seconds) /* Ignore servers sitting idle */ - AND (psComp.value_delta * 10) > ps.value_delta /* Recompilations are more than 10% of batch requests per second */ + AND psComp.value_delta > 75 /* Because sp_BlitzFirst does around 50 compilations and re-compilations */ + AND (psComp.value_delta > (10 * @Seconds) OR psComp.value_delta > ps.value_delta) /* Either doing 10 recompilations per second, or more recompilations than queries */ + AND (psComp.value_delta * 10) > ps.value_delta; /* Recompilations are more than 10% of batch requests per second */ /* Table Problems - Forwarded Fetches/Sec High - CheckID 29 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 29',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 29 AS CheckID, 40 AS Priority, 'Table Problems' AS FindingGroup, 'Forwarded Fetches/Sec High' AS Finding, - 'https://BrentOzar.com/go/fetch/' AS URL, - CAST(ps.value_delta AS NVARCHAR(20)) + ' Forwarded Records (from SQLServer:Access Methods counter)' + @LineFeed + 'https://www.brentozar.com/go/fetch/' AS URL, + CAST(ps.value_delta AS NVARCHAR(20)) + ' forwarded fetches (from SQLServer:Access Methods counter)' + @LineFeed + 'Check your heaps: they need to be rebuilt, or they need a clustered index applied.' + @LineFeed AS Details, 'Rebuild your heaps. If you use Ola Hallengren maintenance scripts, those do not rebuild heaps by default: https://www.brentozar.com/archive/2016/07/fix-forwarded-records/' AS HowToStopIt FROM #PerfmonStats ps @@ -2163,34 +3293,72 @@ BEGIN WHERE ps.Pass = 2 AND ps.object_name = @ServiceName + ':Access Methods' AND ps.counter_name = 'Forwarded Records/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ + + /* Check for temp objects with high forwarded fetches. + This has to be done as dynamic SQL because we have to execute OBJECT_NAME inside TempDB. */ + IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 29) + BEGIN + SET @StringToExecute = N' + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT TOP 10 29 AS CheckID, + 40 AS Priority, + ''Table Problems'' AS FindingGroup, + ''Forwarded Fetches/Sec High: TempDB Object'' AS Finding, + ''https://www.brentozar.com/go/fetch/'' AS URL, + CAST(COALESCE(os.forwarded_fetch_count,0) - COALESCE(os_prior.forwarded_fetch_count,0) AS NVARCHAR(20)) + '' forwarded fetches on '' + + CASE WHEN OBJECT_NAME(os.object_id) IS NULL THEN ''an unknown table '' + WHEN LEN(OBJECT_NAME(os.object_id)) < 50 THEN ''a table variable, internal identifier '' + OBJECT_NAME(os.object_id) + ELSE ''a temp table '' + OBJECT_NAME(os.object_id) + END AS Details, + ''Look through your source code to find the object creating these objects, and tune the creation and population to reduce fetches. See the URL for details.'' AS HowToStopIt + FROM tempdb.sys.dm_db_index_operational_stats(DB_ID(''tempdb''), NULL, NULL, NULL) os + LEFT OUTER JOIN #TempdbOperationalStats os_prior ON os.object_id = os_prior.object_id + AND os.forwarded_fetch_count > os_prior.forwarded_fetch_count + WHERE os.database_id = DB_ID(''tempdb'') + AND os.forwarded_fetch_count - COALESCE(os_prior.forwarded_fetch_count,0) > 100 + ORDER BY os.forwarded_fetch_count DESC;' + EXECUTE sp_executesql @StringToExecute; + END /* In-Memory OLTP - Garbage Collection in Progress - CheckID 31 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 31',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 31 AS CheckID, 50 AS Priority, 'In-Memory OLTP' AS FindingGroup, 'Garbage Collection in Progress' AS Finding, - 'https://BrentOzar.com/go/garbage/' AS URL, + 'https://www.brentozar.com/go/garbage/' AS URL, CAST(ps.value_delta AS NVARCHAR(50)) + ' rows processed (from SQL Server YYYY XTP Garbage Collection:Rows processed/sec counter)' + @LineFeed - + 'This can happen due to memory pressure (causing In-Memory OLTP to shrink its footprint) or' + @LineFeed - + 'due to transactional workloads that constantly insert/delete data.' AS Details, + + 'This can happen for a few reasons: ' + @LineFeed + + 'Memory-Optimized TempDB, or ' + @LineFeed + + 'transactional workloads that constantly insert/delete data in In-Memory OLTP tables, or ' + @LineFeed + + 'memory pressure (causing In-Memory OLTP to shrink its footprint) or' AS Details, 'Sadly, you cannot choose when garbage collection occurs. This is one of the many gotchas of Hekaton. Learn more: http://nedotter.com/archive/2016/04/row-version-lifecycle-for-in-memory-oltp/' AS HowToStopIt FROM #PerfmonStats ps INNER JOIN #PerfmonStats psComp ON psComp.Pass = 2 AND psComp.object_name LIKE '%XTP Garbage Collection' AND psComp.counter_name = 'Rows processed/sec' AND psComp.value_delta > 100 WHERE ps.Pass = 2 AND ps.object_name LIKE '%XTP Garbage Collection' AND ps.counter_name = 'Rows processed/sec' - AND ps.value_delta > (100 * @Seconds) /* Ignore servers sitting idle */ + AND ps.value_delta > (100 * @Seconds); /* Ignore servers sitting idle */ /* In-Memory OLTP - Transactions Aborted - CheckID 32 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 32',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 32 AS CheckID, 100 AS Priority, 'In-Memory OLTP' AS FindingGroup, 'Transactions Aborted' AS Finding, - 'https://BrentOzar.com/go/aborted/' AS URL, + 'https://www.brentozar.com/go/aborted/' AS URL, CAST(ps.value_delta AS NVARCHAR(50)) + ' transactions aborted (from SQL Server YYYY XTP Transactions:Transactions aborted/sec counter)' + @LineFeed + 'This may indicate that data is changing, or causing folks to retry their transactions, thereby increasing load.' AS Details, 'Dig into your In-Memory OLTP transactions to figure out which ones are failing and being retried.' AS HowToStopIt @@ -2199,15 +3367,20 @@ BEGIN WHERE ps.Pass = 2 AND ps.object_name LIKE '%XTP Transactions' AND ps.counter_name = 'Transactions aborted/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ /* Query Problems - Suboptimal Plans/Sec High - CheckID 33 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 33',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) SELECT 32 AS CheckID, 100 AS Priority, 'Query Problems' AS FindingGroup, 'Suboptimal Plans/Sec High' AS Finding, - 'https://BrentOzar.com/go/suboptimal/' AS URL, + 'https://www.brentozar.com/go/suboptimal/' AS URL, CAST(ps.value_delta AS NVARCHAR(50)) + ' plans reported in the ' + CAST(ps.instance_name AS NVARCHAR(100)) + ' workload group (from Workload GroupStats:Suboptimal plans/sec counter)' + @LineFeed + 'Even if you are not using Resource Governor, it still tracks information about user queries, memory grants, etc.' AS Details, 'Check out sp_BlitzCache to get more information about recent queries, or try sp_BlitzWho to see currently running queries.' AS HowToStopIt @@ -2216,18 +3389,98 @@ BEGIN WHERE ps.Pass = 2 AND ps.object_name = @ServiceName + ':Workload GroupStats' AND ps.counter_name = 'Suboptimal plans/sec' - AND ps.value_delta > (10 * @Seconds) /* Ignore servers sitting idle */ + AND ps.value_delta > (10 * @Seconds); /* Ignore servers sitting idle */ + /* Azure Performance - Database is Maxed Out - CheckID 41 */ + IF SERVERPROPERTY('EngineEdition') = 5 /*SERVERPROPERTY('Edition') = 'SQL Azure'*/ + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 41',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt) + SELECT 41 AS CheckID, + 10 AS Priority, + 'Azure Performance' AS FindingGroup, + 'Database is Maxed Out' AS Finding, + 'https://www.brentozar.com/go/maxedout' AS URL, + N'At ' + CONVERT(NVARCHAR(100), s.end_time ,121) + N', your database approached (or hit) your DTU limits:' + @LineFeed + + N'Average CPU percent: ' + CAST(avg_cpu_percent AS NVARCHAR(50)) + @LineFeed + + N'Average data IO percent: ' + CAST(avg_data_io_percent AS NVARCHAR(50)) + @LineFeed + + N'Average log write percent: ' + CAST(avg_log_write_percent AS NVARCHAR(50)) + @LineFeed + + N'Max worker percent: ' + CAST(max_worker_percent AS NVARCHAR(50)) + @LineFeed + + N'Max session percent: ' + CAST(max_session_percent AS NVARCHAR(50)) AS Details, + 'Tune your queries or indexes with sp_BlitzCache or sp_BlitzIndex, or consider upgrading to a higher DTU level.' AS HowToStopIt + FROM sys.dm_db_resource_stats s + WHERE s.end_time >= DATEADD(MI, -5, GETDATE()) + AND (avg_cpu_percent > 90 + OR avg_data_io_percent >= 90 + OR avg_log_write_percent >=90 + OR max_worker_percent >= 90 + 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 + RAISERROR('Running CheckID 19',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) SELECT 19 AS CheckID, 250 AS Priority, 'Server Info' AS FindingGroup, 'Batch Requests per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(CAST(ps.value_delta AS MONEY) / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt FROM #PerfmonStats ps INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 @@ -2236,96 +3489,213 @@ BEGIN AND ps.counter_name = 'Batch Requests/sec'; - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL) - INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL) + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Compilations/sec', NULL); + INSERT INTO #PerfmonCounters ([object_name],[counter_name],[instance_name]) VALUES (@ServiceName + ':SQL Statistics','SQL Re-Compilations/sec', NULL); /* Server Info - SQL Compilations/sec - CheckID 25 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 25 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Compilations/sec'; + IF @ExpertMode >= 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 25',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 25 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Compilations/sec'; + END /* Server Info - SQL Re-Compilations/sec - CheckID 26 */ - IF @ExpertMode = 1 - INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 26 AS CheckID, - 250 AS Priority, - 'Server Info' AS FindingGroup, - 'SQL Re-Compilations per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, - ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt - FROM #PerfmonStats ps - INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 - WHERE ps.Pass = 2 - AND ps.object_name = @ServiceName + ':SQL Statistics' - AND ps.counter_name = 'SQL Re-Compilations/sec'; + IF @ExpertMode >= 1 + BEGIN + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 26',10,1) WITH NOWAIT; + END + + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) + SELECT 26 AS CheckID, + 250 AS Priority, + 'Server Info' AS FindingGroup, + 'SQL Re-Compilations per Sec' AS Finding, + 'https://www.brentozar.com/go/measure' AS URL, + CAST(ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS NVARCHAR(20)) AS Details, + ps.value_delta / (DATEDIFF(ss, ps1.SampleTime, ps.SampleTime)) AS DetailsInt + FROM #PerfmonStats ps + INNER JOIN #PerfmonStats ps1 ON ps.object_name = ps1.object_name AND ps.counter_name = ps1.counter_name AND ps1.Pass = 1 + WHERE ps.Pass = 2 + AND ps.object_name = @ServiceName + ':SQL Statistics' + AND ps.counter_name = 'SQL Re-Compilations/sec'; + END /* Server Info - Wait Time per Core per Sec - CheckID 20 */ IF @Seconds > 0 - BEGIN + BEGIN; + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 20',10,1) WITH NOWAIT; + END; + WITH waits1(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws1.wait_time_ms) FROM #WaitStats ws1 WHERE ws1.Pass = 1 GROUP BY SampleTime), waits2(SampleTime, waits_ms) AS (SELECT SampleTime, SUM(ws2.wait_time_ms) FROM #WaitStats ws2 WHERE ws2.Pass = 2 GROUP BY SampleTime), cores(cpu_count) AS (SELECT SUM(1) FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, DetailsInt) - SELECT 19 AS CheckID, + SELECT 20 AS CheckID, 250 AS Priority, 'Server Info' AS FindingGroup, 'Wait Time per Core per Sec' AS Finding, - 'http://www.BrentOzar.com/go/measure' AS URL, - CAST((waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS NVARCHAR(20)) AS Details, - (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime) AS DetailsInt + 'https://www.brentozar.com/go/measure' AS URL, + CAST((CAST(waits2.waits_ms - waits1.waits_ms AS MONEY)) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS NVARCHAR(20)) AS Details, + (waits2.waits_ms - waits1.waits_ms) / 1000 / i.cpu_count / ISNULL(NULLIF(DATEDIFF(ss, waits1.SampleTime, waits2.SampleTime), 0), 1) AS DetailsInt FROM cores i CROSS JOIN waits1 CROSS JOIN waits2; - END + END; + + IF @Seconds > 0 + BEGIN + INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details) + SELECT + 47 AS CheckId, + 50 AS Priority, + 'Query Problems' AS FindingsGroup, + 'High Percentage Of Runnable Queries' AS Finding, + 'https://erikdarlingdata.com/go/RunnableQueue/' AS URL, + 'On the ' + + CASE WHEN y.pass = 1 + THEN '1st' + ELSE '2nd' + END + + ' pass, ' + + RTRIM(y.runnable_pct) + + '% of your queries were waiting to get on a CPU to run. ' + + ' This can indicate CPU pressure.' + FROM + ( + SELECT + 2 AS pass, + x.total, + x.runnable, + CONVERT(decimal(5,2), + ( + x.runnable / + (1. * NULLIF(x.total, 0)) + ) + ) * 100. AS runnable_pct + FROM + ( + SELECT + COUNT_BIG(*) AS total, + SUM(CASE WHEN status = 'runnable' + THEN 1 + ELSE 0 + END) AS runnable + FROM sys.dm_exec_requests + WHERE session_id > 50 + ) AS x + ) AS y + WHERE y.runnable_pct > 20.; + END - /* Server Performance - High CPU Utilization CheckID 24 */ + /* If we're waiting 30+ seconds, run these checks at the end. + We get this data from the ring buffers, and it's only updated once per minute, so might + as well get it now - whereas if we're checking 30+ seconds, it might get updated by the + end of our sp_BlitzFirst session. */ IF @Seconds >= 30 - BEGIN - /* If we're waiting 30+ seconds, run this check at the end. - We get this data from the ring buffers, and it's only updated once per minute, so might - as well get it now - whereas if we're checking 30+ seconds, it might get updated by the - end of our sp_BlitzFirst session. */ + BEGIN + /* Server Performance - High CPU Utilization CheckID 24 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 24',10,1) WITH NOWAIT; + 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, 'http://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) + BEGIN + RAISERROR('Running CheckID 23',10,1) WITH NOWAIT; + 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, 'http://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 */ + END; /* IF @Seconds >= 30 */ + IF /* Let people on <2016 know about the thread time column */ + ( + @Seconds > 0 + AND @total_cpu_usage = 0 + ) + BEGIN + INSERT INTO + #BlitzFirstResults + ( + CheckID, + Priority, + FindingsGroup, + Finding, + Details, + URL + ) + SELECT + 48, + 254, + N'Informational', + N'Thread Time comes from the plan cache in versions earlier than 2016, and is not as reliable', + N'The oldest plan in your cache is from ' + + CONVERT(nvarchar(30), MIN(s.creation_time)) + + N' and your server was last restarted on ' + + CONVERT(nvarchar(30), MAX(o.sqlserver_start_time)), + N'https://docs.microsoft.com/en-us/sql/relational-databases/system-dynamic-management-views/sys-dm-os-schedulers-transact-sql' + FROM sys.dm_exec_query_stats AS s + CROSS JOIN sys.dm_os_sys_info AS o + OPTION(RECOMPILE); + END /* Let people on <2016 know about the thread time column */ /* If we didn't find anything, apologize. */ IF NOT EXISTS (SELECT * FROM #BlitzFirstResults WHERE Priority < 250) @@ -2347,7 +3717,7 @@ BEGIN 'Try running our more in-depth checks with sp_Blitz, or there may not be an unusual SQL Server performance problem. ' ); - END /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ + END; /*IF NOT EXISTS (SELECT * FROM #BlitzFirstResults) */ /* Add credits for the nice folks who put so much time into building and maintaining this for free: */ INSERT INTO #BlitzFirstResults @@ -2383,7 +3753,12 @@ BEGIN 'We hope you found this tool useful.' ); - /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old */ + /* Outdated sp_BlitzFirst - sp_BlitzFirst is Over 6 Months Old - CheckID 27 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 27',10,1) WITH NOWAIT; + END + IF DATEDIFF(MM, @VersionDate, SYSDATETIMEOFFSET()) > 6 BEGIN INSERT INTO #BlitzFirstResults @@ -2399,8 +3774,14 @@ BEGIN 'Outdated sp_BlitzFirst' AS FindingsGroup , 'sp_BlitzFirst is Over 6 Months Old' AS Finding , 'http://FirstResponderKit.org/' AS URL , - 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details - END + 'Some things get better with age, like fine wine and your T-SQL. However, sp_BlitzFirst is not one of those things - time to go download the current one.' AS Details; + END; + + IF @CheckServerInfo = 0 /* Github #1680 */ + BEGIN + DELETE #BlitzFirstResults + WHERE FindingsGroup = 'Server Info'; + END RAISERROR('Analysis finished, outputting results',10,1) WITH NOWAIT; @@ -2413,56 +3794,9 @@ BEGIN FROM sys.databases WHERE QUOTENAME([name]) = @OutputDatabaseName) BEGIN - RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; - - /* Set the sp_BlitzCache sort order based on their top wait type */ - - /* First, check for poison waits - CheckID 30 */ - IF EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 30) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: RESOURCE_SEMAPHORE_QUERY_COMPILE' THEN 'memory grant' - WHEN Finding = 'Poison Wait Detected: THREADPOOL' THEN 'executions' - WHEN Finding = 'Poison Wait Detected: LOG_RATE_GOVERNOR' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_CATCHUP_THROTTLE' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_COMMIT_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_ROLLBACK_ACK' THEN 'writes' - WHEN Finding = 'Poison Wait Detected: SE_REPL_SLOW_SECONDARY_THROTTLE' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 30 - ORDER BY DetailsInt DESC; - END - - /* Too much free memory - which probably indicates queries finished w/huge grants - CheckID 34 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 34) - SET @BlitzCacheSortOrder = 'memory grant'; - /* Next, Compilations/Sec High - CheckID 15 and 16 */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID IN (15,16)) - SET @BlitzCacheSortOrder = 'compilations'; - - /* Still not set? Use the top wait type. */ - IF @BlitzCacheSortOrder IS NULL AND EXISTS (SELECT * FROM #BlitzFirstResults WHERE CheckID = 6) - BEGIN - SELECT TOP 1 @BlitzCacheSortOrder = CASE - WHEN Finding = 'ASYNC_NETWORK_IO' THEN 'duration' - WHEN Finding = 'CXPACKET' THEN 'reads' - WHEN Finding = 'LATCH_EX' THEN 'reads' - WHEN Finding LIKE 'LCK%' THEN 'duration' - WHEN Finding LIKE 'PAGEIOLATCH%' THEN 'reads' - WHEN Finding = 'SOS_SCHEDULER_YIELD' THEN 'cpu' - WHEN Finding = 'WRITELOG' THEN 'writes' - ELSE NULL - END - FROM #BlitzFirstResults - WHERE CheckID = 6 - ORDER BY DetailsInt DESC; - END - /* Still null? Just use the default. */ + RAISERROR('Calling sp_BlitzCache',10,1) WITH NOWAIT; /* If they have an newer version of sp_BlitzCache that supports @MinutesBack and @CheckDateOverride */ @@ -2474,11 +3808,12 @@ BEGIN /* Get the most recent sp_BlitzCache execution before this one - don't use sp_BlitzFirst because user logs are added in there at any time */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName - + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' - + @OutputSchemaName + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' + + '.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + ''' AND QUOTENAME(TABLE_NAME) = ''' + + QUOTENAME(@OutputTableNameBlitzCache) + ''') SELECT TOP 1 @BlitzCacheMinutesBack = DATEDIFF(MI,CheckDate,SYSDATETIMEOFFSET()) FROM ' + @OutputDatabaseName + '.' + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache + + QUOTENAME(@OutputTableNameBlitzCache) + ' WHERE ServerName = ''' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) + ''' ORDER BY CheckDate DESC;'; EXEC sp_executesql @StringToExecute, N'@BlitzCacheMinutesBack INT OUTPUT', @BlitzCacheMinutesBack OUTPUT; @@ -2486,23 +3821,34 @@ BEGIN IF @BlitzCacheMinutesBack IS NULL OR @BlitzCacheMinutesBack < 1 OR @BlitzCacheMinutesBack > 60 SET @BlitzCacheMinutesBack = 15; - IF @BlitzCacheSortOrder IS NOT NULL + IF(@OutputType = 'NONE') + BEGIN EXEC sp_BlitzCache @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzCache, @CheckDateOverride = @StartSampleTime, - @SortOrder = @BlitzCacheSortOrder, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; - ELSE - EXEC sp_BlitzCache + @Debug = @Debug, + @OutputType = @OutputType + ; + END; + ELSE + BEGIN + + EXEC sp_BlitzCache @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzCache, @CheckDateOverride = @StartSampleTime, + @SortOrder = 'all', + @SkipAnalysis = @BlitzCacheSkipAnalysis, @MinutesBack = @BlitzCacheMinutesBack, - @Debug = @Debug; + @Debug = @Debug + ; + END; /* Delete history older than @OutputTableRetentionDays */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -2511,17 +3857,23 @@ BEGIN + @OutputSchemaName + ''') DELETE ' + @OutputDatabaseName + '.' + @OutputSchemaName + '.' - + @OutputTableNameBlitzCache - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); + + QUOTENAME(@OutputTableNameBlitzCache) + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - END + END; - ELSE /* No sp_BlitzCache found, or it's outdated */ + ELSE BEGIN + /* No sp_BlitzCache found, or it's outdated - CheckID 36 */ + IF (@Debug = 1) + BEGIN + RAISERROR('Running CheckID 36',10,1) WITH NOWAIT; + END + INSERT INTO #BlitzFirstResults ( CheckID , Priority , @@ -2535,12 +3887,12 @@ BEGIN 'Outdated or Missing sp_BlitzCache' AS FindingsGroup , 'Update Your sp_BlitzCache' AS Finding , 'http://FirstResponderKit.org/' AS URL , - 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details - END + 'You passed in @OutputTableNameBlitzCache, but we need a newer version of sp_BlitzCache in master or the current database.' AS Details; + END; RAISERROR('sp_BlitzCache Finished',10,1) WITH NOWAIT; - END /* End running sp_BlitzCache */ + END; /* End running sp_BlitzCache */ /* @OutputTableName lets us export the results to a permanent table */ IF @OutputDatabaseName IS NOT NULL @@ -2586,9 +3938,26 @@ BEGIN DatabaseName NVARCHAR(128) NULL, OpenTransactionCount INT NULL, DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; EXEC(@StringToExecute); + + /* If the table doesn't have the new QueryHash column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''QueryHash'') + ALTER TABLE ' + @ObjectFullName + N' ADD QueryHash BINARY(8) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2164. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' @@ -2596,10 +3965,12 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + (CONVERT(NVARCHAR(100), @StartSampleTime, 121)) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt, QueryHash FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; /* Delete history older than @OutputTableRetentionDays */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -2609,13 +3980,13 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableName - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); - + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - END + END; ELSE IF (SUBSTRING(@OutputTableName, 2, 2) = '##') BEGIN SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' @@ -2644,18 +4015,22 @@ BEGIN DatabaseName NVARCHAR(128) NULL, OpenTransactionCount INT NULL, DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + QueryHash BINARY(8) NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));' + ' INSERT ' + @OutputTableName - + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; - EXEC(@StringToExecute); - END + + ' (ServerName, CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt) SELECT ' + + ' @SrvName, @CheckDate, CheckID, Priority, FindingsGroup, Finding, URL, LEFT(Details,4000), HowToStopIt, QueryPlan, QueryText, StartTime, LoginName, NTUserName, OriginalLoginName, ProgramName, HostName, DatabaseID, DatabaseName, OpenTransactionCount, DetailsInt FROM #BlitzFirstResults ORDER BY Priority , FindingsGroup , Finding , Details'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; /* @OutputTableNameFileStats lets us export the results to a permanent table */ IF @OutputDatabaseName IS NOT NULL @@ -2696,11 +4071,23 @@ BEGIN num_of_writes BIGINT , bytes_written BIGINT, PhysicalName NVARCHAR(520) , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); + PRIMARY KEY CLUSTERED (ID ASC));'; + + EXEC(@StringToExecute); - /* Create the view */ SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameFileStats_View + N';'; + + EXEC(@StringToExecute); + END + + /* Create the view */ IF OBJECT_ID(@ObjectFullName) IS NULL BEGIN SET @StringToExecute = 'USE ' @@ -2708,24 +4095,64 @@ BEGIN + '; EXEC (''CREATE VIEW ' + @OutputSchemaName + '.' + @OutputTableNameFileStats_View + ' AS ' + @LineFeed - + 'SELECT f.ServerName, f.CheckDate, f.DatabaseID, f.DatabaseName, f.FileID, f.FileLogicalName, f.TypeDesc, f.PhysicalName, f.SizeOnDiskMB' + @LineFeed - + ', DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth' + @LineFeed - + ', (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms' + @LineFeed - + ', io_stall_read_ms_average = CASE WHEN (f.num_of_reads - fPrior.num_of_reads) = 0 THEN 0 ELSE (f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads) END' + @LineFeed - + ', (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads' + @LineFeed - + ', (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read' + @LineFeed - + ', (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms' + @LineFeed - + ', io_stall_write_ms_average = CASE WHEN (f.num_of_writes - fPrior.num_of_writes) = 0 THEN 0 ELSE (f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes) END' + @LineFeed - + ', (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes' + @LineFeed - + ', (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName AND f.DatabaseID = fPrior.DatabaseID AND f.FileID = fPrior.FileID AND f.CheckDate > fPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fMiddle ON f.ServerName = fMiddle.ServerName AND f.DatabaseID = fMiddle.DatabaseID AND f.FileID = fMiddle.FileID AND f.CheckDate > fMiddle.CheckDate AND fMiddle.CheckDate > fPrior.CheckDate' + @LineFeed - + 'WHERE fMiddle.ID IS NULL AND f.num_of_reads >= fPrior.num_of_reads AND f.num_of_writes >= fPrior.num_of_writes - AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + ' SELECT f.ServerName,' + @LineFeed + + ' f.CheckDate,' + @LineFeed + + ' f.DatabaseID,' + @LineFeed + + ' f.DatabaseName,' + @LineFeed + + ' f.FileID,' + @LineFeed + + ' f.FileLogicalName,' + @LineFeed + + ' f.TypeDesc,' + @LineFeed + + ' f.PhysicalName,' + @LineFeed + + ' f.SizeOnDiskMB,' + @LineFeed + + ' DATEDIFF(ss, fPrior.CheckDate, f.CheckDate) AS ElapsedSeconds,' + @LineFeed + + ' (f.SizeOnDiskMB - fPrior.SizeOnDiskMB) AS SizeOnDiskMBgrowth,' + @LineFeed + + ' (f.io_stall_read_ms - fPrior.io_stall_read_ms) AS io_stall_read_ms,' + @LineFeed + + ' io_stall_read_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_reads - fPrior.num_of_reads) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_read_ms - fPrior.io_stall_read_ms) / (f.num_of_reads - fPrior.num_of_reads)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_reads - fPrior.num_of_reads) AS num_of_reads,' + @LineFeed + + ' (f.bytes_read - fPrior.bytes_read) / 1024.0 / 1024.0 AS megabytes_read,' + @LineFeed + + ' (f.io_stall_write_ms - fPrior.io_stall_write_ms) AS io_stall_write_ms,' + @LineFeed + + ' io_stall_write_ms_average = CASE' + @LineFeed + + ' WHEN(f.num_of_writes - fPrior.num_of_writes) = 0' + @LineFeed + + ' THEN 0' + @LineFeed + + ' ELSE(f.io_stall_write_ms - fPrior.io_stall_write_ms) / (f.num_of_writes - fPrior.num_of_writes)' + @LineFeed + + ' END,' + @LineFeed + + ' (f.num_of_writes - fPrior.num_of_writes) AS num_of_writes,' + @LineFeed + + ' (f.bytes_written - fPrior.bytes_written) / 1024.0 / 1024.0 AS megabytes_written, ' + @LineFeed + + ' f.ServerName + CAST(f.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' f' + @LineFeed + + ' INNER HASH JOIN CheckDates DATES ON f.CheckDate = DATES.CheckDate' + @LineFeed + + ' INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameFileStats + ' fPrior ON f.ServerName = fPrior.ServerName' + @LineFeed + + ' AND f.DatabaseID = fPrior.DatabaseID' + @LineFeed + + ' AND f.FileID = fPrior.FileID' + @LineFeed + + ' AND fPrior.CheckDate = DATES.PreviousCheckDate' + @LineFeed + + '' + @LineFeed + + ' WHERE f.num_of_reads >= fPrior.num_of_reads' + @LineFeed + + ' AND f.num_of_writes >= fPrior.num_of_writes' + @LineFeed + + ' AND DATEDIFF(MI, fPrior.CheckDate, f.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END; + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName @@ -2734,11 +4161,12 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; /* Delete history older than @OutputTableRetentionDays */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -2748,12 +4176,13 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNameFileStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; - END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + END; ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 2) = '##') BEGIN SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' @@ -2777,19 +4206,20 @@ BEGIN bytes_written BIGINT, PhysicalName NVARCHAR(520) , DetailsInt INT NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + PRIMARY KEY CLUSTERED (ID ASC));' + ' INSERT ' + @OutputTableNameFileStats - + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END + + ' (ServerName, CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName) SELECT ' + + ' @SrvName, @CheckDate, DatabaseID, FileID, DatabaseName, FileLogicalName, TypeDesc, SizeOnDiskMB, io_stall_read_ms, num_of_reads, bytes_read, io_stall_write_ms, num_of_writes, bytes_written, PhysicalName FROM #FileStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; ELSE IF (SUBSTRING(@OutputTableNameFileStats, 2, 1) = '#') BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; /* @OutputTableNamePerfmonStats lets us export the results to a permanent table */ @@ -2825,11 +4255,23 @@ BEGIN [cntr_type] INT NOT NULL, [value_delta] BIGINT NULL, [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' - EXEC(@StringToExecute); + PRIMARY KEY CLUSTERED (ID ASC));'; + + EXEC(@StringToExecute); - /* Create the view */ SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStats_View + N';'; + + EXEC(@StringToExecute); + END + + /* Create the view */ IF OBJECT_ID(@ObjectFullName) IS NULL BEGIN SET @StringToExecute = 'USE ' @@ -2837,19 +4279,188 @@ BEGIN + '; EXEC (''CREATE VIEW ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + ' AS ' + @LineFeed - + 'SELECT p.ServerName, p.CheckDate, p.object_name, p.counter_name, p.instance_name' + @LineFeed - + ', DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS ElapsedSeconds' + @LineFeed - + ', p.cntr_value' + @LineFeed - + ', p.cntr_type' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) AS cntr_delta' + @LineFeed - + ', (p.cntr_value - pPrior.cntr_value) * 1.0 / DATEDIFF(ss, pPrior.CheckDate, p.CheckDate) AS cntr_delta_per_second' + @LineFeed - + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' p' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pPrior ON p.ServerName = pPrior.ServerName AND p.object_name = pPrior.object_name AND p.counter_name = pPrior.counter_name AND p.instance_name = pPrior.instance_name AND p.CheckDate > pPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats + ' pMiddle ON p.ServerName = pMiddle.ServerName AND p.object_name = pMiddle.object_name AND p.counter_name = pMiddle.counter_name AND p.instance_name = pMiddle.instance_name AND p.CheckDate > pMiddle.CheckDate AND pMiddle.CheckDate > pPrior.CheckDate' + @LineFeed - + 'WHERE pMiddle.ID IS NULL AND DATEDIFF(MI, pPrior.CheckDate, p.CheckDate) BETWEEN 1 AND 60;'')' + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + '' + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + + 'SELECT' + @LineFeed + + ' pMon.[ServerName]' + @LineFeed + + ' ,pMon.[CheckDate]' + @LineFeed + + ' ,pMon.[object_name]' + @LineFeed + + ' ,pMon.[counter_name]' + @LineFeed + + ' ,pMon.[instance_name]' + @LineFeed + + ' ,DATEDIFF(SECOND,pMonPrior.[CheckDate],pMon.[CheckDate]) AS ElapsedSeconds' + @LineFeed + + ' ,pMon.[cntr_value]' + @LineFeed + + ' ,pMon.[cntr_type]' + @LineFeed + + ' ,(pMon.[cntr_value] - pMonPrior.[cntr_value]) AS cntr_delta' + @LineFeed + + ' ,(pMon.cntr_value - pMonPrior.cntr_value) * 1.0 / DATEDIFF(ss, pMonPrior.CheckDate, pMon.CheckDate) AS cntr_delta_per_second' + @LineFeed + + ' ,pMon.ServerName + CAST(pMon.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMon' + @LineFeed + + ' INNER HASH JOIN CheckDates Dates' + @LineFeed + + ' ON Dates.CheckDate = pMon.CheckDate' + @LineFeed + + ' JOIN ' + @OutputSchemaName + '.' +@OutputTableNamePerfmonStats + ' pMonPrior' + @LineFeed + + ' ON Dates.PreviousCheckDate = pMonPrior.CheckDate' + @LineFeed + + ' AND pMon.[ServerName] = pMonPrior.[ServerName] ' + @LineFeed + + ' AND pMon.[object_name] = pMonPrior.[object_name] ' + @LineFeed + + ' AND pMon.[counter_name] = pMonPrior.[counter_name] ' + @LineFeed + + ' AND pMon.[instance_name] = pMonPrior.[instance_name]' + @LineFeed + + ' WHERE DATEDIFF(MI, pMonPrior.CheckDate, pMon.CheckDate) BETWEEN 1 AND 60;'')' + + EXEC(@StringToExecute); + END + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_View + N';'; + EXEC(@StringToExecute); + END + + /* Create the second view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = 'USE ' + + @OutputDatabaseName + + '; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNamePerfmonStatsActuals_View + ' AS ' + @LineFeed + + 'WITH PERF_AVERAGE_BULK AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CASE WHEN CHARINDEX(''''('''', counter_name) = 0 THEN counter_name ELSE LEFT (counter_name, CHARINDEX(''''('''',counter_name)-1) END AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + @LineFeed + + ' WHERE cntr_type IN(1073874176)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_LARGE_RAW_BASE AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' LEFT(counter_name, CHARINDEX(''''BASE'''', UPPER(counter_name))-1) AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(1073939712)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_AVERAGE_FRACTION AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' counter_name AS counter_join,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(537003264)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_BULK_COUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_delta / ElapsedSeconds AS cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(272696576, 272696320)' + @LineFeed + + ' AND cntr_delta <> 0' + @LineFeed + + '),' + @LineFeed + + 'PERF_COUNTER_RAWCOUNT AS' + @LineFeed + + '(' + @LineFeed + + ' SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats_View + '' + @LineFeed + + ' WHERE cntr_type IN(65792, 65536)' + @LineFeed + + ')' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' NUM.cntr_delta / DEN.cntr_delta AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + ' ' + @LineFeed + + 'FROM PERF_AVERAGE_BULK AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT NUM.ServerName,' + @LineFeed + + ' NUM.object_name,' + @LineFeed + + ' NUM.counter_name,' + @LineFeed + + ' NUM.instance_name,' + @LineFeed + + ' NUM.CheckDate,' + @LineFeed + + ' CAST((CAST(NUM.cntr_delta as DECIMAL(19)) / DEN.cntr_delta) as decimal(23,3)) AS cntr_value,' + @LineFeed + + ' NUM.ServerName + CAST(NUM.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_AVERAGE_FRACTION AS NUM' + @LineFeed + + ' JOIN PERF_LARGE_RAW_BASE AS DEN ON NUM.counter_join = DEN.counter_join' + @LineFeed + + ' AND NUM.CheckDate = DEN.CheckDate' + @LineFeed + + ' AND NUM.ServerName = DEN.ServerName' + @LineFeed + + ' AND NUM.object_name = DEN.object_name' + @LineFeed + + ' AND NUM.instance_name = DEN.instance_name' + @LineFeed + + ' AND DEN.cntr_delta <> 0' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_BULK_COUNT' + @LineFeed + + '' + @LineFeed + + 'UNION ALL' + @LineFeed + + '' + @LineFeed + + 'SELECT ServerName,' + @LineFeed + + ' object_name,' + @LineFeed + + ' counter_name,' + @LineFeed + + ' instance_name,' + @LineFeed + + ' CheckDate,' + @LineFeed + + ' cntr_value,' + @LineFeed + + ' ServerName + CAST(CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + + 'FROM PERF_COUNTER_RAWCOUNT;'')'; + + EXEC(@StringToExecute); END; + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + @OutputDatabaseName + '.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' @@ -2857,11 +4468,12 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; /* Delete history older than @OutputTableRetentionDays */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -2871,14 +4483,15 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNamePerfmonStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; - END + + END; ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 2) = '##') BEGIN SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' @@ -2895,19 +4508,21 @@ BEGIN [cntr_type] INT NOT NULL, [value_delta] BIGINT NULL, [value_per_second] DECIMAL(18,2) NULL, - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + PRIMARY KEY CLUSTERED (ID ASC));' + ' INSERT ' + @OutputTableNamePerfmonStats - + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ''' + + ' (ServerName, CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second) SELECT ' + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; - EXEC(@StringToExecute); - END + + ' @SrvName, @CheckDate, object_name, counter_name, instance_name, cntr_value, cntr_type, value_delta, value_per_second FROM #PerfmonStats WHERE Pass = 2'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; ELSE IF (SUBSTRING(@OutputTableNamePerfmonStats, 2, 1) = '#') BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; /* @OutputTableNameWaitStats lets us export the results to a permanent table */ @@ -2942,10 +4557,10 @@ BEGIN wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID));' + @LineFeed + PRIMARY KEY CLUSTERED (ID));' + @LineFeed + 'CREATE NONCLUSTERED INDEX IX_ServerName_wait_type_CheckDate_Includes ON ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + '(ServerName, wait_type, CheckDate) INCLUDE (wait_time_ms, signal_wait_time_ms, waiting_tasks_count);' + @LineFeed - + 'END' + + 'END'; EXEC(@StringToExecute); @@ -2957,9 +4572,10 @@ BEGIN + @OutputDatabaseName + '; EXEC (''CREATE TABLE ' + @OutputSchemaName + '.' - + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')' - EXEC(@StringToExecute); - END + + @OutputTableNameWaitStats_Categories + ' (WaitType NVARCHAR(60) PRIMARY KEY CLUSTERED, WaitCategory NVARCHAR(128) NOT NULL, Ignorable BIT DEFAULT 0);'')'; + + EXEC(@StringToExecute); + END; /* Make sure the wait stats category table has the current number of rows */ SET @StringToExecute = 'USE ' @@ -2968,12 +4584,25 @@ BEGIN + 'BEGIN ' + @LineFeed + 'TRUNCATE TABLE ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + @LineFeed + 'INSERT INTO ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' (WaitType, WaitCategory, Ignorable) SELECT WaitType, WaitCategory, Ignorable FROM ##WaitCategories;' + @LineFeed - + 'END'')' - EXEC(@StringToExecute); + + 'END'')'; + + EXEC(@StringToExecute); - /* Create the wait stats view */ SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View; + + /* If the view exists without the most recently added columns, drop it. See Github #2162. */ + IF OBJECT_ID(@ObjectFullName) IS NOT NULL + BEGIN + SET @StringToExecute = N'USE ' + @OutputDatabaseName + N'; IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + DROP VIEW ' + @OutputSchemaName + N'.' + @OutputTableNameWaitStats_View + N';'; + + EXEC(@StringToExecute); + END + + + /* Create the wait stats view */ IF OBJECT_ID(@ObjectFullName) IS NULL BEGIN SET @StringToExecute = 'USE ' @@ -2981,6 +4610,22 @@ BEGIN + '; EXEC (''CREATE VIEW ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_View + ' AS ' + @LineFeed + + 'WITH RowDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ' + @LineFeed + + ' ROW_NUMBER() OVER (ORDER BY [ServerName], [CheckDate]) ID,' + @LineFeed + + ' [CheckDate]' + @LineFeed + + ' FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + @LineFeed + + ' GROUP BY [ServerName], [CheckDate]' + @LineFeed + + '),' + @LineFeed + + 'CheckDates as' + @LineFeed + + '(' + @LineFeed + + ' SELECT ThisDate.CheckDate,' + @LineFeed + + ' LastDate.CheckDate as PreviousCheckDate' + @LineFeed + + ' FROM RowDates ThisDate' + @LineFeed + + ' JOIN RowDates LastDate' + @LineFeed + + ' ON ThisDate.ID = LastDate.ID + 1' + @LineFeed + + ')' + @LineFeed + 'SELECT w.ServerName, w.CheckDate, w.wait_type, COALESCE(wc.WaitCategory, ''''Other'''') AS WaitCategory, COALESCE(wc.Ignorable,0) AS Ignorable' + @LineFeed + ', DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS ElapsedSeconds' + @LineFeed + ', (w.wait_time_ms - wPrior.wait_time_ms) AS wait_time_ms_delta' + @LineFeed @@ -2988,13 +4633,17 @@ BEGIN + ', (w.wait_time_ms - wPrior.wait_time_ms) / 1000.0 / DATEDIFF(ss, wPrior.CheckDate, w.CheckDate) AS wait_time_minutes_per_minute' + @LineFeed + ', (w.signal_wait_time_ms - wPrior.signal_wait_time_ms) AS signal_wait_time_ms_delta' + @LineFeed + ', (w.waiting_tasks_count - wPrior.waiting_tasks_count) AS waiting_tasks_count_delta' + @LineFeed + + ', w.ServerName + CAST(w.CheckDate AS NVARCHAR(50)) AS JoinKey' + @LineFeed + 'FROM ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' w' + @LineFeed - + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND w.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wMiddle ON w.ServerName = wMiddle.ServerName AND w.wait_type = wMiddle.wait_type AND w.CheckDate > wMiddle.CheckDate AND wMiddle.CheckDate > wPrior.CheckDate' + @LineFeed - + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed - + 'WHERE wMiddle.ID IS NULL AND w.wait_time_ms >= wPrior.wait_time_ms AND DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60;'')' - EXEC(@StringToExecute); - END + + 'INNER HASH JOIN CheckDates Dates' + @LineFeed + + 'ON Dates.CheckDate = w.CheckDate' + @LineFeed + + 'INNER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats + ' wPrior ON w.ServerName = wPrior.ServerName AND w.wait_type = wPrior.wait_type AND Dates.PreviousCheckDate = wPrior.CheckDate' + @LineFeed + + 'LEFT OUTER JOIN ' + @OutputSchemaName + '.' + @OutputTableNameWaitStats_Categories + ' wc ON w.wait_type = wc.WaitType' + @LineFeed + + 'WHERE DATEDIFF(MI, wPrior.CheckDate, w.CheckDate) BETWEEN 1 AND 60' + @LineFeed + + 'AND [w].[wait_time_ms] >= [wPrior].[wait_time_ms];'')' + + EXEC(@StringToExecute); + END; SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -3004,11 +4653,12 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; /* Delete history older than @OutputTableRetentionDays */ SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' @@ -3018,12 +4668,13 @@ BEGIN + @OutputDatabaseName + '.' + @OutputSchemaName + '.' + @OutputTableNameWaitStats - + ' WHERE ServerName = ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''' AND CheckDate < ''' + CAST(CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE) AS NVARCHAR(20)) + ''';'; - EXEC(@StringToExecute); + + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate ;'; - END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @LocalServerName, @OutputTableCleanupDate; + + END; ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 2) = '##') BEGIN SET @StringToExecute = N' IF (OBJECT_ID(''tempdb..' @@ -3037,19 +4688,20 @@ BEGIN wait_time_ms BIGINT, signal_wait_time_ms BIGINT, waiting_tasks_count BIGINT , - CONSTRAINT [PK_' + CAST(NEWID() AS CHAR(36)) + '] PRIMARY KEY CLUSTERED (ID ASC));' + PRIMARY KEY CLUSTERED (ID ASC));' + ' INSERT ' + @OutputTableNameWaitStats - + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ''' - + CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128)) - + ''', ''' + CONVERT(NVARCHAR(100), @StartSampleTime, 121) + ''', ' - + 'wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; - EXEC(@StringToExecute); - END + + ' (ServerName, CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count) SELECT ' + + ' @SrvName, @CheckDate, wait_type, wait_time_ms, signal_wait_time_ms, waiting_tasks_count FROM #WaitStats WHERE Pass = 2 AND wait_time_ms > 0 AND waiting_tasks_count > 0'; + + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', + @LocalServerName, @StartSampleTime; + END; ELSE IF (SUBSTRING(@OutputTableNameWaitStats, 2, 1) = '#') BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0) - END + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; @@ -3063,10 +4715,10 @@ BEGIN IF @OutputType = 'COUNT' AND @SinceStartup = 0 BEGIN SELECT COUNT(*) AS Warnings - FROM #BlitzFirstResults - END + FROM #BlitzFirstResults; + END; ELSE - IF @OutputType = 'Opserver1' AND @SinceStartup = 0 + IF @OutputType = 'Opserver1' AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' BEGIN SELECT r.[Priority] , @@ -3122,8 +4774,8 @@ BEGIN END DESC, r.Finding, r.ID; - END - ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 + END; + ELSE IF @OutputType IN ( 'CSV', 'RSV' ) AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' BEGIN SELECT Result = CAST([Priority] AS NVARCHAR(100)) @@ -3143,8 +4795,45 @@ BEGIN END DESC, Finding, Details; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 + END; + ELSE IF @OutputType = 'Top10' AND @OutputResultSets LIKE N'%WaitStats%' + BEGIN + /* Measure waits in hours */ + ;WITH max_batch AS ( + SELECT MAX(SampleTime) AS SampleTime + FROM #WaitStats + ) + SELECT TOP 10 + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], + 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], + (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits] + 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 SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores + CROSS APPLY (SELECT + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c + LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType + WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 + AND wd2.wait_time_ms-wd1.wait_time_ms > 0 + ORDER BY [Wait Time (Seconds)] DESC; + END; + ELSE IF @ExpertMode = 0 AND @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 0 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' BEGIN SELECT [Priority] , [FindingsGroup] , @@ -3163,16 +4852,17 @@ BEGIN ELSE 0 END DESC, Finding, - ID; - END - ELSE IF @ExpertMode = 0 AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @OutputType <> 'NONE' AND @OutputXMLasNVARCHAR = 1 AND @SinceStartup = 0 AND @OutputResultSets LIKE N'%Findings%' BEGIN SELECT [Priority] , [FindingsGroup] , [Finding] , [URL] , - CAST(@StockDetailsHeader + [Details] + @StockDetailsFooter AS NVARCHAR(MAX)) AS Details, - CAST([HowToStopIt] AS NVARCHAR(MAX)) AS HowToStopIt, + CAST(LEFT(@StockDetailsHeader + [Details] + @StockDetailsFooter,32000) AS TEXT) AS Details, + CAST(LEFT([HowToStopIt],32000) AS TEXT) AS HowToStopIt, CAST([QueryText] AS NVARCHAR(MAX)) AS QueryText, CAST([QueryPlan] AS NVARCHAR(MAX)) AS QueryPlan FROM #BlitzFirstResults @@ -3184,9 +4874,10 @@ BEGIN ELSE 0 END DESC, Finding, - ID; - END - ELSE IF @ExpertMode = 1 + ID, + CAST(Details AS NVARCHAR(4000)); + END; + ELSE IF @ExpertMode >= 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%Findings%' BEGIN IF @SinceStartup = 0 SELECT r.[Priority] , @@ -3242,12 +4933,13 @@ BEGIN ELSE 0 END DESC, r.Finding, - r.ID; + r.ID, + CAST(r.Details AS NVARCHAR(4000)); ------------------------- --What happened: #WaitStats ------------------------- - IF @Seconds = 0 + IF @Seconds = 0 AND @OutputResultSets LIKE N'%WaitStats%' BEGIN /* Measure waits in hours */ ;WITH max_batch AS ( @@ -3257,22 +4949,23 @@ BEGIN SELECT 'WAIT STATS' AS Pattern, b.SampleTime AS [Sample Ended], - CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60.0 AS DECIMAL(18,1)) AS [Hours Sample], + CAST(DATEDIFF(mi,wd1.SampleTime, wd2.SampleTime) / 60. AS DECIMAL(18,1)) AS [Hours Sample], + CAST(c.[Total Thread Time (Seconds)] / 60. / 60. AS DECIMAL(18,1)) AS [Thread Time (Hours)], wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, - CAST(c.[Wait Time (Seconds)] / 60.0 / 60 AS DECIMAL(18,1)) AS [Wait Time (Hours)], - CAST((wd2.wait_time_ms - wd1.wait_time_ms) / 1000.0 / 60 / 60 / 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], + 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], - N'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type AS URL + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON wd2.SampleTime =b.SampleTime @@ -3281,14 +4974,16 @@ BEGIN wd2.SampleTime > wd1.SampleTime CROSS APPLY (SELECT SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 AND wd2.wait_time_ms-wd1.wait_time_ms > 0 ORDER BY [Wait Time (Seconds)] DESC; - END - ELSE + END; + ELSE IF @OutputResultSets LIKE N'%WaitStats%' BEGIN /* Measure waits in seconds */ ;WITH max_batch AS ( @@ -3299,21 +4994,22 @@ BEGIN 'WAIT STATS' AS Pattern, b.SampleTime AS [Sample Ended], DATEDIFF(ss,wd1.SampleTime, wd2.SampleTime) AS [Seconds Sample], + c.[Total Thread Time (Seconds)], wd1.wait_type, COALESCE(wcat.WaitCategory, 'Other') AS wait_category, c.[Wait Time (Seconds)], - 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 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'http://www.brentozar.com/sql/wait-stats/#' + wd1.wait_type AS URL + 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], + N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON wd2.SampleTime =b.SampleTime @@ -3322,8 +5018,10 @@ BEGIN wd2.SampleTime > wd1.SampleTime CROSS APPLY (SELECT SUM(1) AS cpu_count FROM sys.dm_os_schedulers WHERE status = 'VISIBLE ONLINE' AND is_online = 1) AS cores CROSS APPLY (SELECT - CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Wait Time (Seconds)], - CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS NUMERIC(12,1)) AS [Signal Wait Time (Seconds)]) AS c + CAST((wd2.wait_time_ms-wd1.wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Wait Time (Seconds)], + CAST((wd2.signal_wait_time_ms - wd1.signal_wait_time_ms)/1000. AS DECIMAL(18,1)) AS [Signal Wait Time (Seconds)], + CAST((wd2.thread_time_ms - wd1.thread_time_ms)/1000. AS DECIMAL(18,1)) AS [Total Thread Time (Seconds)] + ) AS c LEFT OUTER JOIN ##WaitCategories wcat ON wd1.wait_type = wcat.WaitType WHERE (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 AND wd2.wait_time_ms-wd1.wait_time_ms > 0 @@ -3333,6 +5031,7 @@ BEGIN ------------------------- --What happened: #FileStats ------------------------- + IF @OutputResultSets LIKE N'%FileStats%' WITH readstats AS ( SELECT 'PHYSICAL READS' AS Pattern, ROW_NUMBER() OVER (ORDER BY wd2.avg_stall_read_ms DESC) AS StallRank, @@ -3377,20 +5076,22 @@ BEGIN AND wd1.FileID = wd2.FileID ) SELECT - Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] + Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] FROM readstats - WHERE StallRank <=5 AND [MB Read/Written] > 0 + WHERE StallRank <=20 AND [MB Read/Written] > 0 UNION ALL - SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name] + SELECT Pattern, [Sample Time], [Sample (seconds)], [File Name], [Drive], [# Reads/Writes],[MB Read/Written],[Avg Stall (ms)], [file physical name], [DatabaseName], [StallRank] FROM writestats - WHERE StallRank <=5 AND [MB Read/Written] > 0; + WHERE StallRank <=20 AND [MB Read/Written] > 0 + ORDER BY Pattern, StallRank; ------------------------- --What happened: #PerfmonStats ------------------------- - SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, + IF @OutputResultSets LIKE N'%PerfmonStats%' + SELECT 'PERFMON' AS Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name, pFirst.SampleTime AS FirstSampleTime, pFirst.cntr_value AS FirstSampleValue, pLast.SampleTime AS LastSampleTime, pLast.cntr_value AS LastSampleValue, pLast.cntr_value - pFirst.cntr_value AS ValueDelta, @@ -3399,44 +5100,43 @@ BEGIN INNER JOIN #PerfmonStats pFirst ON pFirst.[object_name] = pLast.[object_name] AND pFirst.counter_name = pLast.counter_name AND (pFirst.instance_name = pLast.instance_name OR (pFirst.instance_name IS NULL AND pLast.instance_name IS NULL)) AND pLast.ID > pFirst.ID WHERE pLast.cntr_value <> pFirst.cntr_value - ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name + ORDER BY Pattern, pLast.[object_name], pLast.counter_name, pLast.instance_name; ------------------------- --What happened: #QueryStats ------------------------- - IF @CheckProcedureCache = 1 + IF @CheckProcedureCache = 1 AND @OutputResultSets LIKE N'%BlitzCache%' BEGIN SELECT qsNow.*, qsFirst.* FROM #QueryStats qsNow INNER JOIN #QueryStats qsFirst ON qsNow.[sql_handle] = qsFirst.[sql_handle] AND qsNow.statement_start_offset = qsFirst.statement_start_offset AND qsNow.statement_end_offset = qsFirst.statement_end_offset AND qsNow.plan_generation_num = qsFirst.plan_generation_num AND qsNow.plan_handle = qsFirst.plan_handle AND qsFirst.Pass = 1 - WHERE qsNow.Pass = 2 - END - ELSE + WHERE qsNow.Pass = 2; + END; + ELSE IF @OutputResultSets LIKE N'%BlitzCache%' BEGIN - SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details] - END - END + SELECT 'Plan Cache' AS [Pattern], 'Plan cache not analyzed' AS [Finding], 'Use @CheckProcedureCache = 1 or run sp_BlitzCache for more analysis' AS [More Info], CONVERT(XML, @StockDetailsHeader + 'firstresponderkit.org' + @StockDetailsFooter) AS [Details]; + END; + END; DROP TABLE #BlitzFirstResults; /* What's running right now? This is the first and last result set. */ - IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 -IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 + IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' AND @OutputResultSets LIKE N'%BlitzWho_End%' BEGIN IF OBJECT_ID('master.dbo.sp_BlitzWho') IS NULL AND OBJECT_ID('dbo.sp_BlitzWho') IS NULL BEGIN - PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org' - END + PRINT N'sp_BlitzWho is not installed in the current database_files. You can get a copy from http://FirstResponderKit.org'; + END; ELSE BEGIN - EXEC (@BlitzWho) - END - END /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 - What's running right now? This is the first and last result set. */ + EXEC (@BlitzWho); + END; + END; /* IF @SinceStartup = 0 AND @Seconds > 0 AND @ExpertMode = 1 AND @OutputType <> 'NONE' - What's running right now? This is the first and last result set. */ -END /* IF @LogMessage IS NULL */ -END /* ELSE IF @OutputType = 'SCHEMA' */ +END; /* IF @LogMessage IS NULL */ +END; /* ELSE IF @OutputType = 'SCHEMA' */ SET NOCOUNT OFF; GO @@ -3451,11 +5151,13 @@ EXEC dbo.sp_BlitzFirst @ExpertMode = 1; Saving output to tables: EXEC sp_BlitzFirst -, @OutputDatabaseName = 'DBAtools' + @OutputDatabaseName = 'DBAtools' , @OutputSchemaName = 'dbo' , @OutputTableName = 'BlitzFirst' , @OutputTableNameFileStats = 'BlitzFirst_FileStats' , @OutputTableNamePerfmonStats = 'BlitzFirst_PerfmonStats' , @OutputTableNameWaitStats = 'BlitzFirst_WaitStats' , @OutputTableNameBlitzCache = 'BlitzCache' +, @OutputTableNameBlitzWho = 'BlitzWho' +, @OutputType = 'none' */ diff --git a/sp_BlitzIndex.sql b/sp_BlitzIndex.sql index 4dd2e279a..c748f8e07 100644 --- a/sp_BlitzIndex.sql +++ b/sp_BlitzIndex.sql @@ -13,35 +13,54 @@ IF OBJECT_ID('dbo.sp_BlitzIndex') IS NULL GO 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 */ /*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, + @IgnoreDatabases NVARCHAR(MAX) = NULL, /* Comma-delimited list of databases you want to skip */ @ThresholdMB INT = 250 /* Number of megabytes that an object must be before we include it in basic results */, + @OutputType VARCHAR(20) = 'TABLE' , @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 */, + @SortOrder NVARCHAR(50) = NULL, /* Only affects @Mode = 2. */ + @SortDirection NVARCHAR(4) = 'DESC', /* Only affects @Mode = 2. */ @Help TINYINT = 0, - @VersionDate DATETIME = NULL OUTPUT + @Debug BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; +SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; -DECLARE @Version VARCHAR(30); -SET @Version = '6.0'; -SET @VersionDate = '20171201'; +SELECT @Version = '8.26', @VersionDate = '20251002'; +SET @OutputType = UPPER(@OutputType); +IF(@VersionCheckMode = 1) +BEGIN + RETURN; +END; -IF @Help = 1 PRINT ' +IF @Help = 1 +BEGIN +PRINT ' /* sp_BlitzIndex from http://FirstResponderKit.org @@ -53,12 +72,6 @@ the findings, contribute your own code, and more. Known limitations of this version: - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - The @OutputDatabaseName parameters are not functional yet. To check the - status of this enhancement request, visit: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/221 - - Does not analyze columnstore, spatial, XML, or full text indexes. If you - would like to contribute code to analyze those, head over to Github and - check out the issues list: http://FirstResponderKit.org - Index create statements are just to give you a rough idea of the syntax. It includes filters and fillfactor. -- Example 1: index creates use ONLINE=? instead of ONLINE=ON / ONLINE=OFF. This is because it is important for the user to understand if it is going to be offline and not just run a script. @@ -66,18 +79,15 @@ Known limitations of this version: filegroup/partition scheme etc.) -- (The compression and filegroup index create syntax is not trivial because it is set at the partition level and is not trivial to code.) - - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of insanity.) + - Does not advise you about data modeling for clustered indexes and primary keys (primarily looks for signs of problems.) Unknown limitations of this version: - We knew them once, but we forgot. -Changes - for the full list of improvements and fixes in this version, see: -https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/milestone/4?closed=1 - MIT License -Copyright (c) 2016 Brent Ozar Unlimited +Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -97,7 +107,8 @@ 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. '; - +RETURN; +END; /* @Help = 1 */ DECLARE @ScriptVersionName NVARCHAR(50); DECLARE @DaysUptime NUMERIC(23,2); @@ -115,15 +126,165 @@ DECLARE @FilterMB INT; DECLARE @collation NVARCHAR(256); DECLARE @NumDatabases INT; DECLARE @LineFeed NVARCHAR(5); +DECLARE @DaysUptimeInsertValue NVARCHAR(256); +DECLARE @DatabaseToIgnore NVARCHAR(MAX); +DECLARE @ColumnList NVARCHAR(MAX); +DECLARE @ColumnListWithApostrophes NVARCHAR(MAX); +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'_'); +SET @SortDirection = LOWER(@SortDirection); SET @LineFeed = CHAR(13) + CHAR(10); SELECT @SQLServerProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); SELECT @SQLServerEdition =CAST(SERVERPROPERTY('EngineEdition') AS INT); /* We default to online index creates where EngineEdition=3*/ SET @FilterMB=250; SELECT @ScriptVersionName = 'sp_BlitzIndex(TM) v' + @Version + ' - ' + DATENAME(MM, @VersionDate) + ' ' + RIGHT('0'+DATENAME(DD, @VersionDate),2) + ', ' + DATENAME(YY, @VersionDate); +SET @IgnoreDatabases = REPLACE(REPLACE(LTRIM(RTRIM(@IgnoreDatabases)), CHAR(10), ''), CHAR(13), ''); + +SELECT + @OptimizeForSequentialKey = + CASE WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.all_columns AS ac + WHERE ac.object_id = OBJECT_ID('sys.indexes') + AND ac.name = N'optimize_for_sequential_key' + ) + THEN 1 + ELSE 0 + END; RAISERROR(N'Starting run. %s', 0,1, @ScriptVersionName) WITH NOWAIT; + +IF(@OutputType NOT IN ('TABLE','NONE')) +BEGIN + RAISERROR('Invalid value for parameter @OutputType. Expected: (TABLE;NONE)',12,1); + RETURN; +END; + +IF(@UsualStatisticsSamplingPercent <= 0 OR @UsualStatisticsSamplingPercent > 100) +BEGIN + RAISERROR('Invalid value for parameter @UsualStatisticsSamplingPercent. Expected: 1 to 100',12,1); + 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; + SET @OutputType = 'NONE' +END; + +IF(@OutputType = 'NONE') +BEGIN + + IF ((@OutputServerName IS NOT NULL) AND (@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL)) + BEGIN + RAISERROR('Parameter @OutputServerName is specified, rest of @Output* parameters needs to also be specified',12,1); + RETURN; + END; + + IF(@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL) + BEGIN + RAISERROR('This procedure should be called with a value for @OutputTableName, @OutputSchemaName and @OutputDatabaseName parameters, as @OutputType is set to NONE',12,1); + RETURN; + END; + /* Output is supported for all modes, no reason to not bring pain and output + IF(@BringThePain = 1) + BEGIN + RAISERROR('Incompatible Parameters: @BringThePain set to 1 and @OutputType set to NONE',12,1); + RETURN; + END; + */ + /* Eventually limit by mode + IF(@Mode not in (0,4)) + BEGIN + RAISERROR('Incompatible Parameters: @Mode set to %d and @OutputType set to NONE',12,1,@Mode); + RETURN; + END; + */ +END; + IF OBJECT_ID('tempdb..#IndexSanity') IS NOT NULL DROP TABLE #IndexSanity; @@ -142,6 +303,9 @@ IF OBJECT_ID('tempdb..#MissingIndexes') IS NOT NULL IF OBJECT_ID('tempdb..#ForeignKeys') IS NOT NULL DROP TABLE #ForeignKeys; +IF OBJECT_ID('tempdb..#UnindexedForeignKeys') IS NOT NULL + DROP TABLE #UnindexedForeignKeys; + IF OBJECT_ID('tempdb..#BlitzIndexResults') IS NOT NULL DROP TABLE #BlitzIndexResults; @@ -165,7 +329,24 @@ IF OBJECT_ID('tempdb..#TraceStatus') IS NOT NULL IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL DROP TABLE #TemporalTables; - + +IF OBJECT_ID('tempdb..#CheckConstraints') IS NOT NULL + DROP TABLE #CheckConstraints; + +IF OBJECT_ID('tempdb..#FilteredIndexes') IS NOT NULL + DROP TABLE #FilteredIndexes; + +IF OBJECT_ID('tempdb..#Ignore_Databases') IS NOT NULL + 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 + DROP TABLE #dm_db_index_operational_stats + RAISERROR (N'Create temp tables.',0,1) WITH NOWAIT; CREATE TABLE #BlitzIndexResults ( @@ -173,17 +354,18 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL check_id INT NOT NULL, index_sanity_id INT NULL, Priority INT NULL, - findings_group VARCHAR(4000) NOT NULL, - finding VARCHAR(200) NOT NULL, - [database_name] VARCHAR(200) NULL, - URL VARCHAR(200) NOT NULL, - details NVARCHAR(4000) NOT NULL, + findings_group NVARCHAR(4000) NOT NULL, + finding NVARCHAR(200) NOT NULL, + [database_name] NVARCHAR(128) NULL, + URL NVARCHAR(200) NOT NULL, + details NVARCHAR(MAX) NOT NULL, index_definition NVARCHAR(MAX) NOT NULL, secret_columns NVARCHAR(MAX) NULL, index_usage_summary NVARCHAR(MAX) NULL, index_size_summary NVARCHAR(MAX) NULL, create_tsql NVARCHAR(MAX) NULL, - more_info NVARCHAR(MAX)NULL + more_info NVARCHAR(MAX) NULL, + sample_query_plan XML NULL ); CREATE TABLE #IndexSanity @@ -206,13 +388,17 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL count_included_columns INT NULL , partition_key_column_name NVARCHAR(MAX) NULL, filter_definition NVARCHAR(MAX) NOT NULL , - is_indexed_view BIT NOT NULL , + optimize_for_sequential_key BIT NULL, + is_indexed_view BIT NOT NULL , is_unique BIT NOT NULL , is_primary_key BIT NOT NULL , - is_XML BIT NOT NULL, + is_unique_constraint BIT NOT NULL , + is_XML bit 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 , is_padded BIT NOT NULL , @@ -230,11 +416,12 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL count_secret_columns INT NULL, create_date DATETIME NOT NULL, modify_date DATETIME NOT NULL, - [db_schema_object_name] AS [schema_name] + '.' + [object_name] , - [db_schema_object_indexid] AS [schema_name] + '.' + [object_name] - + CASE WHEN [index_name] IS NOT NULL THEN '.' + index_name - ELSE '' - END + ' (' + CAST(index_id AS NVARCHAR(20)) + ')' , + filter_columns_not_in_index NVARCHAR(MAX), + [db_schema_object_name] AS [schema_name] + N'.' + [object_name] , + [db_schema_object_indexid] AS [schema_name] + N'.' + [object_name] + + CASE WHEN [index_name] IS NOT NULL THEN N'.' + index_name + ELSE N'' + END + N' (' + CAST(index_id AS NVARCHAR(20)) + N')' , first_key_column_name AS CASE WHEN count_key_columns > 1 THEN LEFT(key_column_names, CHARINDEX(',', key_column_names, 0) - 1) ELSE key_column_names @@ -247,20 +434,23 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL CASE index_id WHEN 0 THEN N'[HEAP] ' WHEN 1 THEN N'[CX] ' - ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN '[VIEW] ' + ELSE N'' END + CASE WHEN is_indexed_view = 1 THEN N'[VIEW] ' ELSE N'' END + CASE WHEN is_primary_key = 1 THEN N'[PK] ' 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] ' - ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 THEN N'[UNIQUE] ' + ELSE N'' END + CASE WHEN is_unique = 1 AND is_primary_key = 0 AND is_unique_constraint = 0 THEN N'[UNIQUE] ' + ELSE N'' END + CASE WHEN is_unique_constraint = 1 AND is_primary_key = 0 THEN N'[UNIQUE CONSTRAINT] ' ELSE N'' END + CASE WHEN count_key_columns > 0 THEN - N'[' + CAST(count_key_columns AS VARCHAR(10)) + N' KEY' + N'[' + CAST(count_key_columns AS NVARCHAR(10)) + N' KEY' + CASE WHEN count_key_columns > 1 THEN N'S' ELSE N'' END + N'] ' + LTRIM(key_column_names_with_sort_order) ELSE N'' END + CASE WHEN count_included_columns > 0 THEN - N' [' + CAST(count_included_columns AS VARCHAR(10)) + N' INCLUDE' + + N' [' + CAST(count_included_columns AS NVARCHAR(10)) + N' INCLUDE' + + CASE WHEN count_included_columns > 1 THEN N'S' ELSE N'' END + N'] ' + include_column_names ELSE N'' END + CASE WHEN filter_definition <> N'' THEN N' [FILTER] ' + filter_definition @@ -269,21 +459,31 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL [reads_per_write] AS CAST(CASE WHEN user_updates > 0 THEN ( user_seeks + user_scans + user_lookups ) / (1.0 * user_updates) ELSE 0 END AS MONEY) , - [index_usage_summary] AS N'Reads: ' + - REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), '.00', '') - + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN - N' (' - + RTRIM( - CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), '.00', '') + N' seek ' ELSE N'' END - + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), '.00', '') + N' scan ' ELSE N'' END - + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), '.00', '') + N' lookup' ELSE N'' END - ) - + N') ' - ELSE N' ' END - + N'Writes:' + - REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), '.00', ''), - [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([object_name],'''') + N';' + [index_usage_summary] AS + CASE WHEN is_spatial = 1 THEN N'Not Tracked' + WHEN is_disabled = 1 THEN N'Disabled' + ELSE N'Reads: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks + user_scans + user_lookups) AS MONEY), 1), N'.00', N'') + + CASE WHEN user_seeks + user_scans + user_lookups > 0 THEN + N' (' + + RTRIM( + CASE WHEN user_seeks > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_seeks) AS MONEY), 1), N'.00', N'') + N' seek ' ELSE N'' END + + CASE WHEN user_scans > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_scans) AS MONEY), 1), N'.00', N'') + N' scan ' ELSE N'' END + + CASE WHEN user_lookups > 0 THEN REPLACE(CONVERT(NVARCHAR(30),CAST((user_lookups) AS MONEY), 1), N'.00', N'') + N' lookup' ELSE N'' END + ) + + N') ' + ELSE N' ' + END + + N'Writes: ' + + REPLACE(CONVERT(NVARCHAR(30),CAST(user_updates AS MONEY), 1), N'.00', N'') + END /* First "end" is about is_spatial */, + [more_info] AS + CASE WHEN is_in_memory_oltp = 1 + THEN N'EXEC dbo.sp_BlitzInMemoryOLTP @dbName=' + QUOTENAME([database_name],N'''') + + N', @tableName=' + QUOTENAME([object_name],N'''') + N';' + ELSE N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') + + N', @SchemaName=' + QUOTENAME([schema_name],N'''') + N', @TableName=' + QUOTENAME([object_name],N'''') + N';' + END ); RAISERROR (N'Adding UQ index on #IndexSanity (database_id, object_id, index_id)',0,1) WITH NOWAIT; IF NOT EXISTS(SELECT 1 FROM tempdb.sys.indexes WHERE name='uq_database_id_object_id_index_id') @@ -303,6 +503,7 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL reserved_MB NUMERIC(29,2) NOT NULL , reserved_LOB_MB NUMERIC(29,2) NOT NULL , reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + reserved_dictionary_MB NUMERIC(29,2) NOT NULL , leaf_insert_count BIGINT NULL , leaf_delete_count BIGINT NULL , leaf_update_count BIGINT NULL , @@ -321,7 +522,12 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL page_lock_wait_in_ms BIGINT NULL , index_lock_promotion_attempt_count BIGINT NULL , index_lock_promotion_count BIGINT NULL, - data_compression_desc VARCHAR(60) NULL + data_compression_desc NVARCHAR(60) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL ); CREATE TABLE #IndexSanitySize @@ -335,6 +541,7 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL total_reserved_MB NUMERIC(29,2) NOT NULL , total_reserved_LOB_MB NUMERIC(29,2) NOT NULL , total_reserved_row_overflow_MB NUMERIC(29,2) NOT NULL , + total_reserved_dictionary_MB NUMERIC(29,2) NOT NULL , total_leaf_delete_count BIGINT NULL, total_leaf_update_count BIGINT NULL, total_range_scan_count BIGINT NULL, @@ -350,7 +557,12 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL avg_page_lock_wait_in_ms BIGINT NULL , total_index_lock_promotion_attempt_count BIGINT NULL , total_index_lock_promotion_count BIGINT NULL , - data_compression_desc VARCHAR(8000) NULL, + data_compression_desc NVARCHAR(4000) NULL, + page_latch_wait_count BIGINT NULL, + page_latch_wait_in_ms BIGINT NULL, + page_io_latch_wait_count BIGINT NULL, + page_io_latch_wait_in_ms BIGINT NULL, + lock_escalation_desc nvarchar(60) NULL, index_size_summary AS ISNULL( CASE WHEN partition_count > 1 THEN N'[' + CAST(partition_count AS NVARCHAR(10)) + N' PARTITIONS] ' @@ -362,9 +574,9 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL CAST(CAST(total_reserved_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB' END + CASE WHEN total_reserved_LOB_MB > 1024 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB LOB' + N'; ' + CAST(CAST(total_reserved_LOB_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END WHEN total_reserved_LOB_MB > 0 THEN - N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB LOB' + N'; ' + CAST(CAST(total_reserved_LOB_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB ' + CASE WHEN total_reserved_dictionary_MB = 0 THEN N'LOB' ELSE N'Columnstore' END ELSE '' END + CASE WHEN total_reserved_row_overflow_MB > 1024 THEN @@ -372,6 +584,12 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL WHEN total_reserved_row_overflow_MB > 0 THEN N'; ' + CAST(CAST(total_reserved_row_overflow_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Row Overflow' ELSE '' + END + + CASE WHEN total_reserved_dictionary_MB > 1024 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'GB Dictionaries' + WHEN total_reserved_dictionary_MB > 0 THEN + N'; ' + CAST(CAST(total_reserved_dictionary_MB AS NUMERIC(29,1)) AS NVARCHAR(30)) + N'MB Dictionaries' + ELSE '' END , N'Error- NULL in computed column'), index_op_stats AS ISNULL( @@ -388,7 +606,10 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL ), N'Table metadata not in memory'), index_lock_wait_summary AS ISNULL( CASE WHEN total_row_lock_wait_count = 0 AND total_page_lock_wait_count = 0 AND - total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits.' + total_index_lock_promotion_attempt_count = 0 THEN N'0 lock waits; ' + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN N'Lock escalation DISABLE.' + ELSE N'' + END ELSE CASE WHEN total_row_lock_wait_count > 0 THEN N'Row lock waits: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_row_lock_wait_count AS MONEY), 1), N'.00', N'') @@ -424,7 +645,11 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL END + CASE WHEN total_index_lock_promotion_attempt_count > 0 THEN N'Lock escalation attempts: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(total_index_lock_promotion_attempt_count AS MONEY), 1), N'.00', N'') - + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') + N'.' + + N'; Actual Escalations: ' + REPLACE(CONVERT(NVARCHAR(30),CAST(ISNULL(total_index_lock_promotion_count,0) AS MONEY), 1), N'.00', N'') +N'; ' + ELSE N'' + END + + CASE WHEN lock_escalation_desc = N'DISABLE' THEN + N'Lock escalation is disabled.' ELSE N'' END END @@ -453,9 +678,9 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL is_replicated BIT NULL, is_sparse BIT NULL, is_filestream BIT NULL, - seed_value BIGINT NULL, - increment_value INT NULL , - last_value BIGINT NULL, + seed_value DECIMAL(38,0) NULL, + increment_value DECIMAL(38,0) NULL , + last_value DECIMAL(38,0) NULL, is_not_for_replication BIT NULL ); CREATE CLUSTERED INDEX CLIX_database_id_object_id_index_id ON #IndexColumns @@ -474,9 +699,12 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL user_seeks BIGINT NOT NULL, user_scans BIGINT NOT NULL, unique_compiles BIGINT NULL, - equality_columns NVARCHAR(4000), - inequality_columns NVARCHAR(4000), - included_columns NVARCHAR(4000), + equality_columns NVARCHAR(MAX), + equality_columns_with_data_type NVARCHAR(MAX), + inequality_columns NVARCHAR(MAX), + inequality_columns_with_data_type NVARCHAR(MAX), + included_columns NVARCHAR(MAX), + included_columns_with_data_type NVARCHAR(MAX), is_low BIT, [index_estimated_impact] AS REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( @@ -487,20 +715,24 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL + N'%; Avg query cost: ' + CAST(avg_total_user_cost AS NVARCHAR(30)), [missing_index_details] AS - CASE WHEN equality_columns IS NOT NULL THEN N'EQUALITY: ' + equality_columns + N' ' - ELSE N'' - END + CASE WHEN inequality_columns IS NOT NULL THEN N'INEQUALITY: ' + inequality_columns + N' ' - ELSE N'' - END + CASE WHEN included_columns IS NOT NULL THEN N'INCLUDES: ' + included_columns + N' ' - ELSE N'' - END, - [create_tsql] AS N'CREATE INDEX [ix_' + table_name + N'_' - + REPLACE(REPLACE(REPLACE(REPLACE( + CASE WHEN COALESCE(equality_columns_with_data_type,equality_columns) IS NOT NULL + THEN N'EQUALITY: ' + COALESCE(CAST(equality_columns_with_data_type AS NVARCHAR(MAX)), CAST(equality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(inequality_columns_with_data_type,inequality_columns) IS NOT NULL + THEN N'INEQUALITY: ' + COALESCE(CAST(inequality_columns_with_data_type AS NVARCHAR(MAX)), CAST(inequality_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END + + + CASE WHEN COALESCE(included_columns_with_data_type,included_columns) IS NOT NULL + THEN N'INCLUDE: ' + COALESCE(CAST(included_columns_with_data_type AS NVARCHAR(MAX)), CAST(included_columns AS NVARCHAR(MAX))) + N' ' + ELSE N'' END, + [create_tsql] AS N'CREATE INDEX [' + + LEFT(REPLACE(REPLACE(REPLACE(REPLACE( ISNULL(equality_columns,N'')+ CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N'_' ELSE N'' END + ISNULL(inequality_columns,''),',','') ,'[',''),']',''),' ','_') - + CASE WHEN included_columns IS NOT NULL THEN N'_includes' ELSE N'' END + N'] ON ' + + CASE WHEN included_columns IS NOT NULL THEN N'_Includes' ELSE N'' END, 128) + N'] ON ' + [statement] + N' (' + ISNULL(equality_columns,N'') + CASE WHEN equality_columns IS NOT NULL AND inequality_columns IS NOT NULL THEN N', ' ELSE N'' END + CASE WHEN inequality_columns IS NOT NULL THEN inequality_columns ELSE N'' END + @@ -511,7 +743,8 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL + N';' , [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],'''') + - N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';' + N', @SchemaName=' + QUOTENAME([schema_name],'''') + N', @TableName=' + QUOTENAME([table_name],'''') + N';', + [sample_query_plan] XML NULL ); CREATE TABLE #ForeignKeys ( @@ -531,6 +764,18 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL update_referential_action_desc NVARCHAR(16), delete_referential_action_desc NVARCHAR(60) ); + + CREATE TABLE #UnindexedForeignKeys + ( + [database_id] INT NOT NULL, + [database_name] NVARCHAR(128) NOT NULL , + [schema_name] NVARCHAR(128) NOT NULL , + foreign_key_name NVARCHAR(256), + parent_object_name NVARCHAR(256), + parent_object_id INT, + referenced_object_name NVARCHAR(256), + referenced_object_id INT + ); CREATE TABLE #IndexCreateTsql ( index_sanity_id INT NOT NULL, @@ -545,16 +790,17 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL CREATE TABLE #PartitionCompressionInfo ( [index_sanity_id] INT NULL, - [partition_compression_detail] VARCHAR(8000) NULL + [partition_compression_detail] NVARCHAR(4000) 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, - column_names NVARCHAR(4000) NULL, + column_names NVARCHAR(MAX) NULL, statistics_name NVARCHAR(128) NULL, last_statistics_update DATETIME NULL, days_since_last_stats_update INT NULL, @@ -564,13 +810,15 @@ IF OBJECT_ID('tempdb..#TemporalTables') 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 @@ -592,7 +840,7 @@ IF OBJECT_ID('tempdb..#TemporalTables') IS NOT NULL CREATE TABLE #TraceStatus ( - TraceFlag VARCHAR(10) , + TraceFlag NVARCHAR(10) , status BIT , Global BIT , Session BIT @@ -609,7 +857,94 @@ IF OBJECT_ID('tempdb..#TemporalTables') 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 + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + table_name NVARCHAR(128) NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + constraint_name NVARCHAR(128) NULL, + is_disabled BIT NULL, + definition NVARCHAR(MAX) NULL, + uses_database_collation BIT NOT NULL, + is_not_trusted BIT NOT NULL, + is_function INT NOT NULL, + column_definition NVARCHAR(MAX) NULL + ); + + CREATE TABLE #FilteredIndexes + ( + index_sanity_id INT IDENTITY(1, 1) NOT NULL, + database_name NVARCHAR(128) NULL, + database_id INT NOT NULL, + schema_name NVARCHAR(128) NOT NULL, + table_name NVARCHAR(128) NOT NULL, + index_name NVARCHAR(128) 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), + Reason NVARCHAR(100) ); /* Sanitize our inputs */ @@ -625,89 +960,143 @@ IF @GetAllDatabases = 1 INSERT INTO #DatabaseList (DatabaseName) SELECT DB_NAME(database_id) FROM sys.databases - WHERE user_access_desc='MULTI_USER' + WHERE user_access_desc = 'MULTI_USER' AND state_desc = 'ONLINE' AND database_id > 4 AND DB_NAME(database_id) NOT LIKE 'ReportServer%' - AND is_distributor = 0; + AND DB_NAME(database_id) NOT LIKE 'rdsadmin%' + AND LOWER(name) NOT IN('dbatools', 'dbadmin', 'dbmaintenance') + AND is_distributor = 0 + OPTION ( RECOMPILE ); /* Skip non-readable databases in an AG - see Github issue #1160 */ IF EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id AND o.name = 'dm_hadr_availability_replica_states' AND c.name = 'role_desc') BEGIN SET @dsql = N'UPDATE #DatabaseList SET secondary_role_allow_connections_desc = ''NO'' WHERE DatabaseName IN ( - SELECT d.name + SELECT DB_NAME(d.database_id) FROM sys.dm_hadr_availability_replica_states rs INNER JOIN sys.databases d ON rs.replica_id = d.replica_id INNER JOIN sys.availability_replicas r ON rs.replica_id = r.replica_id WHERE rs.role_desc = ''SECONDARY'' - AND r.secondary_role_allow_connections_desc = ''NO'');'; + AND r.secondary_role_allow_connections_desc = ''NO'') + OPTION ( RECOMPILE );'; EXEC sp_executesql @dsql; IF EXISTS (SELECT * FROM #DatabaseList WHERE secondary_role_allow_connections_desc = 'NO') BEGIN INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'Skipped non-readable AG secondary databases.', - N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', - N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', - 'http://FirstResponderKit.org', '', '', '', '' + VALUES ( 1, + 0, + N'Skipped non-readable AG secondary databases.', + N'You are running this on an AG secondary, and some of your databases are configured as non-readable when this is a secondary node.', + N'To analyze those databases, run sp_BlitzIndex on the primary, or on a readable secondary.', + 'http://FirstResponderKit.org', '', '', '', '' ); END; END; + IF @IgnoreDatabases IS NOT NULL + AND LEN(@IgnoreDatabases) > 0 + BEGIN + RAISERROR(N'Setting up filter to ignore databases', 0, 1) WITH NOWAIT; + SET @DatabaseToIgnore = ''; + + WHILE LEN(@IgnoreDatabases) > 0 + BEGIN + IF PATINDEX('%,%', @IgnoreDatabases) > 0 + BEGIN + SET @DatabaseToIgnore = SUBSTRING(@IgnoreDatabases, 0, PATINDEX('%,%',@IgnoreDatabases)) ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + + SET @IgnoreDatabases = SUBSTRING(@IgnoreDatabases, LEN(@DatabaseToIgnore + ',') + 1, LEN(@IgnoreDatabases)) ; + END; + ELSE + BEGIN + SET @DatabaseToIgnore = @IgnoreDatabases ; + SET @IgnoreDatabases = NULL ; + + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT LTRIM(RTRIM(@DatabaseToIgnore)), 'Specified in the @IgnoreDatabases parameter' + OPTION (RECOMPILE) ; + END; + END; + + END + END; ELSE BEGIN INSERT INTO #DatabaseList ( DatabaseName ) - SELECT CASE WHEN @DatabaseName IS NULL OR @DatabaseName = N'' THEN DB_NAME() + SELECT CASE + WHEN @DatabaseName IS NULL OR @DatabaseName = N'' + THEN DB_NAME() ELSE @DatabaseName END; - END; + END; + +SET @NumDatabases = (SELECT COUNT(*) FROM #DatabaseList AS D LEFT OUTER JOIN #Ignore_Databases AS I ON D.DatabaseName = I.DatabaseName WHERE I.DatabaseName IS NULL AND ISNULL(D.secondary_role_allow_connections_desc, 'YES') != 'NO'); +SET @msg = N'Number of databases to examine: ' + CAST(@NumDatabases AS NVARCHAR(50)); +RAISERROR (@msg,0,1) WITH NOWAIT; + -SET @NumDatabases = @@ROWCOUNT; /* Running on 50+ databases can take a reaaallly long time, so we want explicit permission to do so (and only after warning about it) */ + BEGIN TRY - IF @NumDatabases >= 50 AND @BringThePain != 1 + IF @NumDatabases >= 50 AND @BringThePain != 1 AND @TableName IS NULL BEGIN INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, index_usage_summary, index_size_summary ) - VALUES ( -1, 0 , - @ScriptVersionName, - CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' + VALUES ( -1, + 0 , + @ScriptVersionName, + CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16), GETDATE(), 121) END, + N'http://FirstResponderKit.org', + N'From Your Community Volunteers', + N'', + N'', + N'' ); INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, database_name, URL, details, index_definition, index_usage_summary, index_size_summary ) - VALUES ( 1, 0 , - N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', - N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary insanity for the server and/or user.', - N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', - 'http://FirstResponderKit.org', '', '', '', '' + VALUES ( 1, + 0, + N'You''re trying to run sp_BlitzIndex on a server with ' + CAST(@NumDatabases AS NVARCHAR(8)) + N' databases. ', + N'Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server and/or user.', + '', + 'http://FirstResponderKit.org', + N'If you''re sure you want to do this, run again with the parameter @BringThePain = 1.', + '', + '', + '' ); - - SELECT bir.blitz_result_id, - bir.check_id, - bir.index_sanity_id, - bir.Priority, - bir.findings_group, - bir.finding, - bir.database_name, - bir.URL, - bir.details, - bir.index_definition, - bir.secret_columns, - bir.index_usage_summary, - bir.index_size_summary, - bir.create_tsql, - bir.more_info - FROM #BlitzIndexResults AS bir; + if(@OutputType <> 'NONE') + BEGIN + SELECT bir.blitz_result_id, + bir.check_id, + bir.index_sanity_id, + bir.Priority, + bir.findings_group, + bir.finding, + bir.database_name, + bir.URL, + bir.details, + bir.index_definition, + bir.secret_columns, + bir.index_usage_summary, + bir.index_size_summary, + bir.create_tsql, + bir.more_info + FROM #BlitzIndexResults AS bir; + RAISERROR('Running sp_BlitzIndex on a server with 50+ databases may cause temporary problems for the server', 12, 1); + END; RETURN; @@ -716,12 +1105,11 @@ END TRY BEGIN CATCH RAISERROR (N'Failure to execute due to number of databases.', 0,1) WITH NOWAIT; - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + SELECT @msg = ERROR_MESSAGE(), + @ErrorSeverity = ERROR_SEVERITY(), + @ErrorState = ERROR_STATE(); - RAISERROR (@msg, - @ErrorSeverity, - @ErrorState - ); + RAISERROR (@msg, @ErrorSeverity, @ErrorState); WHILE @@trancount > 0 ROLLBACK; @@ -729,16 +1117,85 @@ BEGIN CATCH RETURN; END CATCH; + +RAISERROR (N'Checking partition counts to exclude databases with over 100 partitions',0,1) WITH NOWAIT; +IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL + BEGIN + DECLARE partition_cursor CURSOR FOR + SELECT dl.DatabaseName + FROM #DatabaseList dl + LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName + WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL + + OPEN partition_cursor + FETCH NEXT FROM partition_cursor INTO @DatabaseName + + WHILE @@FETCH_STATUS = 0 + BEGIN + /* Count the total number of partitions */ + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SELECT @RowcountOUT = SUM(1) FROM ' + QUOTENAME(@DatabaseName) + '.sys.partitions WHERE partition_number > 1 OPTION ( RECOMPILE );'; + EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; + IF @Rowcount > 100 + BEGIN + RAISERROR (N'Skipping database %s because > 100 partitions were found. To check this database, you must set @BringThePain = 1.',0,1,@DatabaseName) WITH NOWAIT; + INSERT INTO #Ignore_Databases (DatabaseName, Reason) + SELECT @DatabaseName, 'Over 100 partitions found - use @BringThePain = 1 to analyze' + END; + FETCH NEXT FROM partition_cursor INTO @DatabaseName + END; + CLOSE partition_cursor + DEALLOCATE partition_cursor + + END; + +INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, + index_usage_summary, index_size_summary ) +SELECT 1, 0 , + 'Database Skipped', + i.DatabaseName, + 'http://FirstResponderKit.org', + i.Reason, '', '', '' +FROM #Ignore_Databases i; + + +/* Last startup */ +IF COLUMNPROPERTY(OBJECT_ID('sys.dm_os_sys_info'),'sqlserver_start_time','ColumnID') IS NOT NULL +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, sqlserver_start_time, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.dm_os_sys_info; +END +ELSE +BEGIN + SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC (23,2)) + FROM sys.databases + WHERE database_id = 2; +END + +IF @DaysUptime = 0 OR @DaysUptime IS NULL + SET @DaysUptime = .01; + +SELECT @DaysUptimeInsertValue = 'Server: ' + (CONVERT(VARCHAR(256), (SERVERPROPERTY('ServerName')))) + ' Days Uptime: ' + RTRIM(@DaysUptime); + + /* Permission granted or unnecessary? Ok, let's go! */ +RAISERROR (N'Starting loop through databases',0,1) WITH NOWAIT; DECLARE c1 CURSOR LOCAL FAST_FORWARD FOR -SELECT DatabaseName FROM #DatabaseList WHERE COALESCE(secondary_role_allow_connections_desc, 'OK') <> 'NO' ORDER BY DatabaseName; +SELECT dl.DatabaseName +FROM #DatabaseList dl +LEFT OUTER JOIN #Ignore_Databases i ON dl.DatabaseName = i.DatabaseName +WHERE COALESCE(dl.secondary_role_allow_connections_desc, 'OK') <> 'NO' + AND i.DatabaseName IS NULL +ORDER BY dl.DatabaseName; OPEN c1; FETCH NEXT FROM c1 INTO @DatabaseName; - WHILE @@FETCH_STATUS = 0 + WHILE @@FETCH_STATUS = 0 + BEGIN RAISERROR (@LineFeed, 0, 1) WITH NOWAIT; @@ -751,21 +1208,16 @@ FROM sys.databases AND user_access_desc='MULTI_USER' AND state_desc = 'ONLINE'; -/* Last startup */ -SELECT @DaysUptime = CAST(DATEDIFF(hh,create_date,GETDATE())/24. AS NUMERIC (23,2)) -FROM sys.databases -WHERE database_id = 2; - -IF @DaysUptime = 0 SET @DaysUptime = .01; - ---------------------------------------- --STEP 1: OBSERVE THE PATIENT --This step puts index information into temp tables. ---------------------------------------- BEGIN TRY BEGIN + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'starting at %s',0,1, @d) WITH NOWAIT; - --Validate SQL Server Verson + --Validate SQL Server Version IF (SELECT LEFT(@SQLServerProductVersion, CHARINDEX('.',@SQLServerProductVersion,0)-1 @@ -797,7 +1249,7 @@ BEGIN TRY IF ((@Mode <> 0 OR @TableName IS NOT NULL) AND @Filter <> 0) BEGIN - SET @msg=N'@Filter only appies when @Mode=0 and @TableName is not specified. Please try again.'; + SET @msg=N'@Filter only applies when @Mode=0 and @TableName is not specified. Please try again.'; RAISERROR(@msg,16,1); END; @@ -821,7 +1273,7 @@ BEGIN TRY BEGIN SET @dsql = N' SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT @ObjectID= OBJECT_ID + SELECT @ObjectID= OBJECT_ID FROM ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS sc on so.schema_id=sc.schema_id @@ -858,7 +1310,9 @@ BEGIN TRY --insert columns for clustered indexes and heaps --collect info on identity columns for this one - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + SET @dsql = N'/* sp_BlitzIndex */ + SET LOCK_TIMEOUT 1000; /* To fix locking bug in sys.identity_columns. See Github issue #2176. */ + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT ' + CAST(@DatabaseID AS NVARCHAR(16)) + ', s.name, si.object_id, @@ -879,9 +1333,9 @@ BEGIN TRY c.is_replicated, ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_sparse' ELSE N'NULL as is_sparse' END + N', ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'c.is_filestream' ELSE N'NULL as is_filestream' END + N', - CAST(ic.seed_value AS BIGINT), - CAST(ic.increment_value AS INT), - CAST(ic.last_value AS BIGINT), + CAST(ic.seed_value AS DECIMAL(38,0)), + CAST(ic.increment_value AS DECIMAL(38,0)), + CAST(ic.last_value AS DECIMAL(38,0)), ic.is_not_for_replication FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes si JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON @@ -909,10 +1363,44 @@ BEGIN TRY RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #IndexColumns for clustered indexes and heaps',0,1) WITH NOWAIT; - INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, - column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, - is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) - EXEC sp_executesql @dsql; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 1, 4000); + PRINT SUBSTRING(@dsql, 4001, 4000); + PRINT SUBSTRING(@dsql, 8001, 4000); + PRINT SUBSTRING(@dsql, 12001, 4000); + PRINT SUBSTRING(@dsql, 16001, 4000); + PRINT SUBSTRING(@dsql, 20001, 4000); + PRINT SUBSTRING(@dsql, 24001, 4000); + PRINT SUBSTRING(@dsql, 28001, 4000); + PRINT SUBSTRING(@dsql, 32001, 4000); + PRINT SUBSTRING(@dsql, 36001, 4000); + END; + BEGIN TRY + INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, + column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, + is_replicated, is_sparse, is_filestream, seed_value, increment_value, last_value, is_not_for_replication ) + EXEC sp_executesql @dsql; + END TRY + BEGIN CATCH + RAISERROR (N'Failure inserting data into #IndexColumns for clustered indexes and heaps.', 0,1) WITH NOWAIT; + + IF @dsql IS NOT NULL + BEGIN + SET @msg= 'Last @dsql: ' + @dsql; + RAISERROR(@msg, 0, 1) WITH NOWAIT; + END; + + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), + @ErrorSeverity = 0, @ErrorState = ERROR_STATE(); + RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; + + WHILE @@trancount > 0 + ROLLBACK; + + RETURN; + END CATCH; + --insert columns for nonclustered indexes --this uses a full join to sys.index_columns @@ -961,66 +1449,113 @@ BEGIN TRY RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #IndexColumns for nonclustered indexes',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; INSERT #IndexColumns ( database_id, [schema_name], [object_id], index_id, key_ordinal, is_included_column, is_descending_key, partition_ordinal, column_name, system_type_name, max_length, precision, scale, collation_name, is_nullable, is_identity, is_computed, is_replicated, is_sparse, is_filestream ) EXEC sp_executesql @dsql; - + SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, so.object_id, si.index_id, si.type, - ' + QUOTENAME(@DatabaseName, '''') + ' AS database_name, - COALESCE(sc.NAME, ''Unknown'') AS [schema_name], + @i_DatabaseName AS database_name, + COALESCE(sc.name, ''Unknown'') AS [schema_name], COALESCE(so.name, ''Unknown'') AS [object_name], COALESCE(si.name, ''Unknown'') AS [index_name], CASE WHEN so.[type] = CAST(''V'' AS CHAR(2)) THEN 1 ELSE 0 END, si.is_unique, si.is_primary_key, - CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, + si.is_unique_constraint, + CASE when si.type = 3 THEN 1 ELSE 0 END AS is_XML, 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, si.is_padded, si.fill_factor,' - + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN ' + + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N' CASE WHEN si.filter_definition IS NOT NULL THEN si.filter_definition - ELSE '''' - END AS filter_definition' ELSE ''''' AS filter_definition' END + ' - , ISNULL(us.user_seeks, 0), ISNULL(us.user_scans, 0), - ISNULL(us.user_lookups, 0), ISNULL(us.user_updates, 0), us.last_user_seek, us.last_user_scan, - us.last_user_lookup, us.last_user_update, - so.create_date, so.modify_date - FROM ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS si WITH (NOLOCK) - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id + ELSE N'''' + END AS filter_definition' ELSE N''''' AS filter_definition' END + + CASE + WHEN @OptimizeForSequentialKey = 1 + THEN N', si.optimize_for_sequential_key' + ELSE N', CONVERT(BIT, NULL) AS optimize_for_sequential_key' + END + + N', + ISNULL(us.user_seeks, 0), + ISNULL(us.user_scans, 0), + ISNULL(us.user_lookups, 0), + ISNULL(us.user_updates, 0), + us.last_user_seek, + us.last_user_scan, + us.last_user_lookup, + us.last_user_update, + so.create_date, + so.modify_date + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS si WITH (NOLOCK) + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so WITH (NOLOCK) ON si.object_id = so.object_id AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas sc ON so.schema_id = sc.schema_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc ON so.schema_id = sc.schema_id 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)) + ' - WHERE si.[type] IN ( 0, 1, 2, 3, 4, 5, 6 ) - /* Heaps, clustered, nonclustered, XML, spatial, Cluster Columnstore, NC Columnstore */ ' + - CASE WHEN @TableName IS NOT NULL THEN ' and so.name=' + QUOTENAME(@TableName,'''') + ' ' ELSE '' END + - 'OPTION ( RECOMPILE ); + AND us.database_id = ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' + 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) + AND @TableName IS NULL ) + THEN N'AND ( us.user_seeks + us.user_scans + us.user_lookups + us.user_updates ) > 0' + ELSE N'' + END + + N'OPTION ( RECOMPILE ); '; IF @dsql IS NULL RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #IndexSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + 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_XML, is_spatial, is_NC_columnstore, is_CX_columnstore, - is_disabled, is_hypothetical, is_padded, fill_factor, filter_definition, user_seeks, user_scans, + 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 ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; RAISERROR (N'Checking partition count',0,1) WITH NOWAIT; - IF @BringThePain = 0 AND @SkipPartitions = 0 + IF @BringThePain = 0 AND @SkipPartitions = 0 AND @TableName IS NULL BEGIN /* Count the total number of partitions */ SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; @@ -1028,14 +1563,14 @@ BEGIN TRY EXEC sp_executesql @dsql, N'@RowcountOUT BIGINT OUTPUT', @RowcountOUT = @Rowcount OUTPUT; IF @Rowcount > 100 BEGIN - RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',16,1) WITH NOWAIT; + RAISERROR (N'Setting @SkipPartitions = 1 because > 100 partitions were found. To check them, you must set @BringThePain = 1.',0,1) WITH NOWAIT; SET @SkipPartitions = 1; INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, index_usage_summary, index_size_summary ) VALUES ( 1, 0 , 'Some Checks Were Skipped', '@SkipPartitions Forced to 1', - 'http://FirstResponderKit.org', CAST(@Rowcount AS VARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' + 'http://FirstResponderKit.org', CAST(@Rowcount AS NVARCHAR(50)) + ' partitions found. To analyze them, use @BringThePain = 1.', 'We try to keep things quick - and warning, running @BringThePain = 1 can take tens of minutes.', '', '' ); END; END; @@ -1051,59 +1586,195 @@ BEGIN TRY RAISERROR (N'Preferring non-2012 syntax with LEFT JOIN to sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; --NOTE: If you want to use the newer syntax for 2012+, you'll have to change 2147483647 to 11 on line ~819 - --This change was made because on a table with lots of paritions, the OUTER APPLY was crazy slow. - SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, + --This change was made because on a table with lots of partitions, the OUTER APPLY was crazy slow. + + -- get relevant columns from sys.dm_db_partition_stats, sys.partitions and sys.objects + IF OBJECT_ID('tempdb..#dm_db_partition_stats_etc') IS NOT NULL + DROP TABLE #dm_db_partition_stats_etc; + + create table #dm_db_partition_stats_etc + ( + database_id smallint not null + , object_id int not null + , sname sysname NULL + , index_id int + , partition_number int + , partition_id bigint + , row_count bigint + , reserved_MB NUMERIC(29,2) + , reserved_LOB_MB NUMERIC(29,2) + , reserved_row_overflow_MB NUMERIC(29,2) + , lock_escalation_desc nvarchar(60) + , data_compression_desc nvarchar(60) + ) + + -- get relevant info from sys.dm_db_index_operational_stats + IF OBJECT_ID('tempdb..#dm_db_index_operational_stats') IS NOT NULL + DROP TABLE #dm_db_index_operational_stats; + create table #dm_db_index_operational_stats + ( + database_id smallint not null + , object_id int not null + , index_id int + , partition_number int + , hobt_id bigint + , leaf_insert_count bigint + , leaf_delete_count bigint + , leaf_update_count bigint + , range_scan_count bigint + , singleton_lookup_count bigint + , forwarded_fetch_count bigint + , lob_fetch_in_pages bigint + , lob_fetch_in_bytes bigint + , row_overflow_fetch_in_pages bigint + , row_overflow_fetch_in_bytes bigint + , row_lock_count bigint + , row_lock_wait_count bigint + , row_lock_wait_in_ms bigint + , page_lock_count bigint + , page_lock_wait_count bigint + , page_lock_wait_in_ms bigint + , index_lock_promotion_attempt_count bigint + , index_lock_promotion_count bigint + , page_latch_wait_count bigint + , page_latch_wait_in_ms bigint + , page_io_latch_wait_count bigint + , page_io_latch_wait_in_ms bigint + ) + + SET @dsql = N' + DECLARE @d VARCHAR(19) = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_partition_stats_etc at %s'',0,1, @d) WITH NOWAIT; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + INSERT INTO #dm_db_partition_stats_etc + ( + database_id, object_id, sname, index_id, partition_number, partition_id, row_count, reserved_MB, reserved_LOB_MB, reserved_row_overflow_MB, lock_escalation_desc, data_compression_desc + ) + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, ps.object_id, - s.name, + s.name as sname, ps.index_id, ps.partition_number, + ps.partition_id, ps.row_count, ps.reserved_page_count * 8. / 1024. AS reserved_MB, ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN 'par.data_compression_desc ' ELSE 'null as data_compression_desc' END + ' - FROM ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_partition_stats AS ps - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.partitions AS par on ps.partition_id=par.partition_id - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.objects AS so ON ps.object_id = so.object_id + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' +'; + + SET @dsql = @dsql + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id - LEFT JOIN ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_operational_stats(' - + CAST(@DatabaseID AS NVARCHAR(10)) + ', NULL, NULL,NULL) AS os ON - ps.object_id=os.object_id and ps.index_id=os.index_id and ps.partition_number=os.partition_number + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le WHERE 1=1 - ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + ' - ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number - OPTION ( RECOMPILE ); + ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' + ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + N' + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''start getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; + + insert into #dm_db_index_operational_stats + ( + database_id + , object_id + , index_id + , partition_number + , hobt_id + , leaf_insert_count + , leaf_delete_count + , leaf_update_count + , range_scan_count + , singleton_lookup_count + , forwarded_fetch_count + , lob_fetch_in_pages + , lob_fetch_in_bytes + , row_overflow_fetch_in_pages + , row_overflow_fetch_in_bytes + , row_lock_count + , row_lock_wait_count + , row_lock_wait_in_ms + , page_lock_count + , page_lock_wait_count + , page_lock_wait_in_ms + , index_lock_promotion_attempt_count + , index_lock_promotion_count + , page_latch_wait_count + , page_latch_wait_in_ms + , page_io_latch_wait_count + , page_io_latch_wait_in_ms + ) + + select os.database_id + , os.object_id + , os.index_id + , os.partition_number ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', os.hobt_id ' + ELSE N', NULL AS hobt_id ' + END + N' + , os.leaf_insert_count + , os.leaf_delete_count + , os.leaf_update_count + , os.range_scan_count + , os.singleton_lookup_count + , os.forwarded_fetch_count + , os.lob_fetch_in_pages + , os.lob_fetch_in_bytes + , os.row_overflow_fetch_in_pages + , os.row_overflow_fetch_in_bytes + , os.row_lock_count + , os.row_lock_wait_count + , os.row_lock_wait_in_ms + , os.page_lock_count + , os.page_lock_wait_count + , os.page_lock_wait_in_ms + , os.index_lock_promotion_attempt_count + , os.index_lock_promotion_count + , os.page_latch_wait_count + , os.page_latch_wait_in_ms + , os.page_io_latch_wait_count + , os.page_io_latch_wait_in_ms + from ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats('+ CAST(@DatabaseID AS NVARCHAR(10)) +', NULL, NULL,NULL) AS os + OPTION ( RECOMPILE ' + + CASE WHEN (PARSENAME(@SQLServerProductVersion, 4) ) > 12 THEN N', min_grant_percent = 1 ' + ELSE N' ' + END + N'); + + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121) + RAISERROR (N''finished getting data into #dm_db_index_operational_stats at %s.'',0,1, @d) WITH NOWAIT; '; END; ELSE BEGIN RAISERROR (N'Using 2012 syntax to query sys.dm_db_index_operational_stats',0,1) WITH NOWAIT; --This is the syntax that will be used if you change 2147483647 to 11 on line ~819. - --If you have a lot of paritions and this suddenly starts running for a long time, change it back. + --If you have a lot of partitions and this suddenly starts running for a long time, change it back. SET @dsql = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + ' AS database_id, + SELECT ' + CAST(@DatabaseID AS NVARCHAR(10)) + N' AS database_id, ps.object_id, s.name, ps.index_id, @@ -1112,37 +1783,66 @@ BEGIN TRY ps.reserved_page_count * 8. / 1024. AS reserved_MB, ps.lob_reserved_page_count * 8. / 1024. AS reserved_LOB_MB, ps.row_overflow_reserved_page_count * 8. / 1024. AS reserved_row_overflow_MB, - os.leaf_insert_count, - os.leaf_delete_count, - os.leaf_update_count, - os.range_scan_count, - os.singleton_lookup_count, - os.forwarded_fetch_count, - os.lob_fetch_in_pages, - os.lob_fetch_in_bytes, - os.row_overflow_fetch_in_pages, - os.row_overflow_fetch_in_bytes, - os.row_lock_count, - os.row_lock_wait_count, - os.row_lock_wait_in_ms, - os.page_lock_count, - os.page_lock_wait_count, - os.page_lock_wait_in_ms, - os.index_lock_promotion_attempt_count, - os.index_lock_promotion_count, - ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N' + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc' END + N', + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms)'; + + /* Get columnstore dictionary size - more info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2585 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'column_store_dictionaries') + SET @dsql = @dsql + N' COALESCE((SELECT SUM (on_disk_size / 1024.0 / 1024) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries dict WHERE dict.partition_id = ps.partition_id),0) AS reserved_dictionary_MB '; + ELSE + SET @dsql = @dsql + N' 0 AS reserved_dictionary_MB '; + + + SET @dsql = @dsql + N' FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_partition_stats AS ps JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions AS par on ps.partition_id=par.partition_id JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects AS so ON ps.object_id = so.object_id AND so.is_ms_shipped = 0 /*Exclude objects shipped by Microsoft*/ AND so.type <> ''TF'' /*Exclude table valued functions*/ - JOIN ' + QUOTENAME(@DatabaseName) + '.sys.schemas AS s ON s.schema_id = so.schema_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s ON s.schema_id = so.schema_id OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_index_operational_stats(' + CAST(@DatabaseID AS NVARCHAR(10)) + N', ps.object_id, ps.index_id,ps.partition_number) AS os + OUTER APPLY (SELECT st.lock_escalation_desc + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.tables st + WHERE st.object_id = ps.object_id + AND ps.index_id < 2 ) le WHERE 1=1 ' + CASE WHEN @ObjectID IS NOT NULL THEN N'AND so.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) + N' ' ELSE N' ' END + N' ' + CASE WHEN @Filter = 2 THEN N'AND ps.reserved_page_count * 8./1024. > ' + CAST(@FilterMB AS NVARCHAR(5)) + N' ' ELSE N' ' END + ' - ORDER BY ps.object_id, ps.index_id, ps.partition_number + GROUP BY ps.object_id, + s.name, + ps.index_id, + ps.partition_number, + ps.partition_id, + ps.row_count, + ps.reserved_page_count, + ps.lob_reserved_page_count, + ps.row_overflow_reserved_page_count, + le.lock_escalation_desc, + ' + CASE WHEN @SQLServerProductVersion NOT LIKE '9%' THEN N'par.data_compression_desc ' ELSE N'null as data_compression_desc ' END + N' + ORDER BY ps.object_id, ps.index_id, ps.partition_number OPTION ( RECOMPILE ); '; END; @@ -1151,6 +1851,20 @@ BEGIN TRY RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #IndexPartitionSanity',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + EXEC sp_executesql @dsql; INSERT #IndexPartitionSanity ( [database_id], [object_id], [schema_name], @@ -1159,7 +1873,9 @@ BEGIN TRY row_count, reserved_MB, reserved_LOB_MB, - reserved_row_overflow_MB, + reserved_row_overflow_MB, + lock_escalation_desc, + data_compression_desc, leaf_insert_count, leaf_delete_count, leaf_update_count, @@ -1177,105 +1893,479 @@ BEGIN TRY page_lock_wait_count, page_lock_wait_in_ms, index_lock_promotion_attempt_count, - index_lock_promotion_count, - data_compression_desc ) - EXEC sp_executesql @dsql; - + index_lock_promotion_count, + page_latch_wait_count, + page_latch_wait_in_ms, + page_io_latch_wait_count, + page_io_latch_wait_in_ms, + reserved_dictionary_MB) + select h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc, + SUM(os.leaf_insert_count), + SUM(os.leaf_delete_count), + SUM(os.leaf_update_count), + SUM(os.range_scan_count), + SUM(os.singleton_lookup_count), + SUM(os.forwarded_fetch_count), + SUM(os.lob_fetch_in_pages), + SUM(os.lob_fetch_in_bytes), + SUM(os.row_overflow_fetch_in_pages), + SUM(os.row_overflow_fetch_in_bytes), + SUM(os.row_lock_count), + SUM(os.row_lock_wait_count), + SUM(os.row_lock_wait_in_ms), + SUM(os.page_lock_count), + SUM(os.page_lock_wait_count), + SUM(os.page_lock_wait_in_ms), + SUM(os.index_lock_promotion_attempt_count), + SUM(os.index_lock_promotion_count), + SUM(os.page_latch_wait_count), + SUM(os.page_latch_wait_in_ms), + SUM(os.page_io_latch_wait_count), + SUM(os.page_io_latch_wait_in_ms) + ,COALESCE((SELECT SUM (dict.on_disk_size / 1024.0 / 1024) FROM sys.column_store_dictionaries dict WHERE dict.partition_id = h.partition_id),0) AS reserved_dictionary_MB + from #dm_db_partition_stats_etc h + left JOIN #dm_db_index_operational_stats as os ON + h.object_id=os.object_id and h.index_id=os.index_id and h.partition_number=os.partition_number + group by h.database_id, h.object_id, h.sname, h.index_id, h.partition_number, h.partition_id, h.row_count, h.reserved_MB, h.reserved_LOB_MB, h.reserved_row_overflow_MB, h.lock_escalation_desc, h.data_compression_desc + END; --End Check For @SkipPartitions = 0 - + IF @Mode NOT IN(1, 2) + BEGIN RAISERROR (N'Inserting data into #MissingIndexes',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT id.database_id, id.object_id, ' + QUOTENAME(@DatabaseName,'''') + N', sc.[name], so.[name], id.statement , gs.avg_total_user_cost, - gs.avg_user_impact, gs.user_seeks, gs.user_scans, gs.unique_compiles,id.equality_columns, - id.inequality_columns,id.included_columns - FROM sys.dm_db_missing_index_groups ig - JOIN sys.dm_db_missing_index_details id ON ig.index_handle = id.index_handle - JOIN sys.dm_db_missing_index_group_stats gs ON ig.index_group_handle = gs.group_handle - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so on - id.object_id=so.object_id - JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc on - so.schema_id=sc.schema_id - WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + ' - ' + CASE WHEN @ObjectID IS NULL THEN N'' - ELSE N'and id.object_id=' + CAST(@ObjectID AS NVARCHAR(30)) - END + - N'OPTION (RECOMPILE);'; + SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + + + SET @dsql = @dsql + ' +WITH + ColumnNamesWithDataTypes AS +( + SELECT + id.index_handle, + id.object_id, + cn.IndexColumnType, + STUFF + ( + ( + SELECT + '', '' + + cn_inner.ColumnName + + '' '' + + N'' {'' + + CASE + WHEN ty.name IN (''varchar'', ''char'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''nvarchar'', ''nchar'') + THEN ty.name + + ''('' + + CASE + WHEN co.max_length = -1 + THEN ''max'' + ELSE CAST(co.max_length / 2 AS VARCHAR(25)) + END + + '')'' + WHEN ty.name IN (''decimal'', ''numeric'') + THEN ty.name + + ''('' + + CAST(co.precision AS VARCHAR(25)) + + '', '' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + WHEN ty.name IN (''datetime2'') + THEN ty.name + + ''('' + + CAST(co.scale AS VARCHAR(25)) + + '')'' + ELSE ty.name END + ''}'' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id_inner + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(N''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES + (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id_inner.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + ) AS cn_inner + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS co + ON co.object_id = id_inner.object_id + AND ''['' + co.name + '']'' = cn_inner.ColumnName + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types AS ty + 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(@i_DatabaseName) + AND cn_inner.IndexColumnType = cn.IndexColumnType + FOR XML PATH('''') + ), + 1, + 1, + '''' + ) AS ReplaceColumnNames + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details AS id + CROSS APPLY + ( + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Equality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.equality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Inequality'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.inequality_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + UNION ALL + SELECT + LTRIM(RTRIM(v.value(''(./text())[1]'', ''varchar(max)''))) AS ColumnName, + ''Included'' AS IndexColumnType + FROM + ( + VALUES (CONVERT(XML, N'''' + REPLACE((SELECT CAST(id.included_columns AS nvarchar(max)) FOR XML PATH('''')), N'','', N'''') + N'''')) + ) x (n) + CROSS APPLY n.nodes(''x'') node(v) + )AS cn + WHERE id.database_id = DB_ID(@i_DatabaseName) + GROUP BY + id.index_handle, + id.object_id, + cn.IndexColumnType +) +SELECT + * +INTO #ColumnNamesWithDataTypes +FROM ColumnNamesWithDataTypes +OPTION(RECOMPILE); + +SELECT + id.database_id, + id.object_id, + @i_DatabaseName, + sc.[name], + so.[name], + id.statement, + gs.avg_total_user_cost, + gs.avg_user_impact, + gs.user_seeks, + gs.user_scans, + gs.unique_compiles, + id.equality_columns, + id.inequality_columns, + id.included_columns, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Equality'' + ) AS equality_columns_with_data_type, + ( + SELECT + ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Inequality'' + ) AS inequality_columns_with_data_type, + ( + SELECT ColumnNamesWithDataTypes.ReplaceColumnNames + FROM #ColumnNamesWithDataTypes ColumnNamesWithDataTypes + WHERE ColumnNamesWithDataTypes.index_handle = id.index_handle + AND ColumnNamesWithDataTypes.object_id = id.object_id + AND ColumnNamesWithDataTypes.IndexColumnType = ''Included'' + ) AS included_columns_with_data_type,'; + + /* Get the sample query plan if it's available, and if there are less than 1,000 rows in the DMV: */ + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.all_objects AS o + WHERE o.name = 'dm_db_missing_index_group_stats_query' + ) + SELECT + @dsql += N' + NULL AS sample_query_plan' + ELSE + BEGIN + /* The DMV is only supposed to have 600 rows in it. If it's got more, + they could see performance slowdowns - see Github #3085. */ + DECLARE @MissingIndexPlans BIGINT; + SET @StringToExecute = N'SELECT @MissingIndexPlans = COUNT(*) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query;' + EXEC sp_executesql @StringToExecute, N'@MissingIndexPlans BIGINT OUT', @MissingIndexPlans OUT; + + IF @MissingIndexPlans > 1000 + BEGIN + SELECT @dsql += N' + NULL AS sample_query_plan /* Over 1000 plans found, skipping */'; + RAISERROR (N'Over 1000 plans found in sys.dm_db_missing_index_group_stats_query - your SQL Server is hitting a bug: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/3085',0,1) WITH NOWAIT; + END + ELSE + SELECT + @dsql += N' + sample_query_plan = + ( + SELECT TOP (1) + p.query_plan + FROM sys.dm_db_missing_index_group_stats gs + CROSS APPLY + ( + SELECT TOP (1) + s.plan_handle + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats_query q + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_exec_query_stats s + ON q.query_plan_hash = s.query_plan_hash + WHERE gs.group_handle = q.group_handle + ORDER BY + (q.user_seeks + q.user_scans) DESC, + s.total_logical_reads DESC + ) q2 + CROSS APPLY sys.dm_exec_query_plan(q2.plan_handle) p + WHERE ig.index_group_handle = gs.group_handle + )' + END + + + + SET @dsql = @dsql + N' +FROM ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_groups ig +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_details id + ON ig.index_handle = id.index_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_missing_index_group_stats gs + ON ig.index_group_handle = gs.group_handle +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects so + ON id.object_id=so.object_id +JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sc + ON so.schema_id=sc.schema_id +WHERE id.database_id = ' + CAST(@DatabaseID AS NVARCHAR(30)) + +CASE + WHEN @ObjectID IS NULL + THEN N'' + ELSE N' +AND id.object_id = ' + CAST(@ObjectID AS NVARCHAR(30)) +END + +N' +OPTION (RECOMPILE);'; IF @dsql IS NULL RAISERROR('@dsql is null',16,1); + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; INSERT #MissingIndexes ( [database_id], [object_id], [database_name], [schema_name], [table_name], [statement], avg_total_user_cost, avg_user_impact, user_seeks, user_scans, unique_compiles, equality_columns, - inequality_columns, included_columns) - EXEC sp_executesql @dsql; + inequality_columns, included_columns, equality_columns_with_data_type, inequality_columns_with_data_type, + included_columns_with_data_type, sample_query_plan) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; SET @dsql = N' - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' 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(''.'', ''varchar(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(''.'', ''varchar(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); RAISERROR (N'Inserting data into #ForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; INSERT #ForeignKeys ( [database_id], [database_name], [schema_name], foreign_key_name, parent_object_id,parent_object_name, referenced_object_id, referenced_object_name, is_disabled, is_not_trusted, is_not_for_replication, parent_fk_columns, referenced_fk_columns, [update_referential_action_desc], [delete_referential_action_desc] ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql = N' + SELECT + 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(@i_DatabaseName)), + fk.parent_object_id, + foreign_key_referenced_table = + 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 + ON s.schema_id = fk.schema_id + WHERE fk.is_disabled = 0 + AND EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.foreign_key_columns fkc + WHERE fkc.constraint_object_id = fk.object_id + AND NOT EXISTS + ( + SELECT + 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic + WHERE ic.object_id = fkc.parent_object_id + AND ic.column_id = fkc.parent_column_id + AND ic.index_column_id = fkc.constraint_column_id + ) + ) + OPTION (RECOMPILE);' + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + + RAISERROR (N'Inserting data into #UnindexedForeignKeys',0,1) WITH NOWAIT; + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + INSERT + #UnindexedForeignKeys + ( + database_id, + database_name, + schema_name, + foreign_key_name, + parent_object_name, + parent_object_id, + referenced_object_name, + referenced_object_id + ) + EXEC sys.sp_executesql + @dsql, + N'@i_DatabaseName sysname', + @DatabaseName; + END; - IF @SkipStatistics = 0 + IF @Mode NOT IN(1, 2) + BEGIN + IF @SkipStatistics = 0 /* AND DB_NAME() = @DatabaseName /* Can only get stats in the current database - see https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1947 */ */ BEGIN IF ((PARSENAME(@SQLServerProductVersion, 4) >= 12) OR (PARSENAME(@SQLServerProductVersion, 4) = 11 AND PARSENAME(@SQLServerProductVersion, 2) >= 3000) OR (PARSENAME(@SQLServerProductVersion, 4) = 10 AND PARSENAME(@SQLServerProductVersion, 3) = 50 AND PARSENAME(@SQLServerProductVersion, 2) >= 2500)) BEGIN RAISERROR (N'Gathering Statistics Info With Newer Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - obj.name AS table_name, - sch.name AS schema_name, + SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + 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, is_incremental) + SELECT DB_ID(@i_DatabaseName) 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, ca.column_names AS column_names, s.name AS statistics_name, @@ -1291,14 +2381,33 @@ BEGIN TRY 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 @@ -1308,13 +2417,13 @@ BEGIN TRY ON i.object_id = s.object_id AND i.index_id = s.stats_id OUTER APPLY ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_stats_properties(s.object_id, s.stats_id) AS ddsp - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON sc.column_id = c.column_id AND sc.object_id = c.object_id WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') ) ca (column_names) WHERE obj.is_ms_shipped = 0 OPTION (RECOMPILE);'; @@ -1323,19 +2432,33 @@ BEGIN TRY RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; END; ELSE BEGIN RAISERROR (N'Gathering Statistics Info With Older Syntax.',0,1) WITH NOWAIT; - SET @dsql=N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - SELECT DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], ' - + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], + SET @dsql=N'USE ' + QUOTENAME(@DatabaseName) + N'; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + 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, is_incremental) + SELECT DB_ID(@i_DatabaseName) 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, @@ -1349,7 +2472,7 @@ BEGIN TRY 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, @@ -1358,13 +2481,24 @@ BEGIN TRY ' + 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 - ON si.name = s.name + ON si.name = s.name AND s.object_id = si.id INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.objects obj ON s.object_id = obj.object_id INNER HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas sch @@ -1372,13 +2506,13 @@ BEGIN TRY LEFT HASH JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i ON i.object_id = s.object_id AND i.index_id = s.stats_id - CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name + CROSS APPLY ( SELECT STUFF((SELECT '', '' + c.name FROM ' + QUOTENAME(@DatabaseName) + N'.sys.stats_columns AS sc JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c ON sc.column_id = c.column_id AND sc.object_id = c.object_id WHERE sc.stats_id = s.stats_id AND sc.object_id = s.object_id ORDER BY sc.stats_column_id - FOR XML PATH(''''), TYPE).value(''.'', ''varchar(max)''), 1, 2, '''') + FOR XML PATH(''''), TYPE).value(''.'', ''nvarchar(max)''), 1, 2, '''') ) ca (column_names) WHERE obj.is_ms_shipped = 0 AND si.rowcnt > 0 @@ -1388,21 +2522,32 @@ BEGIN TRY RAISERROR('@dsql is null',16,1); RAISERROR (N'Inserting data into #Statistics',0,1) WITH NOWAIT; - 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) + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; END; - END; + IF @Mode NOT IN(1, 2) + BEGIN IF (PARSENAME(@SQLServerProductVersion, 4) >= 10) BEGIN RAISERROR (N'Gathering Computed Column Info.',0,1) WITH NOWAIT; - SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS [database_name], - DB_ID(' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, t.name AS table_name, s.name AS schema_name, c.name AS column_name, @@ -1411,7 +2556,7 @@ BEGIN TRY cc.uses_database_collation, cc.is_persisted, cc.is_computed, - CASE WHEN cc.definition LIKE ''%.%'' THEN 1 ELSE 0 END AS is_function, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + '' ADD '' + QUOTENAME(c.name) + '' AS '' + cc.definition + CASE WHEN is_persisted = 1 THEN '' PERSISTED'' ELSE '''' END + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] @@ -1425,16 +2570,17 @@ BEGIN TRY ON s.schema_id = t.schema_id OPTION (RECOMPILE);'; - IF @dsql IS NULL - RAISERROR('@dsql is null',16,1); + IF @dsql IS NULL RAISERROR('@dsql is null',16,1); INSERT #ComputedColumns - ( [database_name], database_id, table_name, schema_name, column_name, is_nullable, definition, + ( database_id, [database_name], table_name, schema_name, column_name, is_nullable, definition, uses_database_collation, is_persisted, is_computed, is_function, column_definition ) - EXEC sp_executesql @dsql; + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END; + END; - END; - + IF @Mode NOT IN(1, 2) + BEGIN RAISERROR (N'Gathering Trace Flag Information',0,1) WITH NOWAIT; INSERT #TraceStatus EXEC ('DBCC TRACESTATUS(-1) WITH NO_INFOMSGS'); @@ -1443,14 +2589,15 @@ BEGIN TRY BEGIN RAISERROR (N'Gathering Temporal Table Info',0,1) WITH NOWAIT; SET @dsql=N'SELECT ' + QUOTENAME(@DatabaseName,'''') + N' AS database_name, - DB_ID(' + 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, 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 @@ -1475,10 +2622,128 @@ BEGIN TRY IF @dsql IS NULL RAISERROR('@dsql is null',16,1); - INSERT #TemporalTables ( database_name, database_id, schema_name, table_name, history_table_name, - history_schema_name, start_column_name, end_column_name, period_name ) + 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], + @i_DatabaseName AS database_name, + t.name AS table_name, + s.name AS schema_name, + cc.name AS constraint_name, + cc.is_disabled, + cc.definition, + cc.uses_database_collation, + cc.is_not_trusted, + CASE WHEN cc.definition LIKE ''%|].|[%'' ESCAPE ''|'' THEN 1 ELSE 0 END AS is_function, + ''ALTER TABLE '' + QUOTENAME(s.name) + ''.'' + QUOTENAME(t.name) + + '' ADD CONSTRAINT '' + QUOTENAME(cc.name) + '' CHECK '' + cc.definition + '';'' COLLATE DATABASE_DEFAULT AS [column_definition] + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.check_constraints AS cc + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = cc.parent_object_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON s.schema_id = t.schema_id + OPTION (RECOMPILE);'; + + INSERT #CheckConstraints + ( database_id, [database_name], table_name, schema_name, constraint_name, is_disabled, definition, + uses_database_collation, is_not_trusted, is_function, column_definition ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + + + IF @Mode NOT IN(1, 2) + BEGIN + SET @dsql=N'SELECT DB_ID(@i_DatabaseName) AS [database_id], + @i_DatabaseName AS database_name, + s.name AS missing_schema_name, + t.name AS missing_table_name, + i.name AS missing_index_name, + c.name AS missing_column_name + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.sql_expression_dependencies AS sed + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.tables AS t + ON t.object_id = sed.referenced_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.schemas AS s + ON t.schema_id = s.schema_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes AS i + ON i.object_id = sed.referenced_id + AND i.index_id = sed.referencing_minor_id + JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns AS c + ON c.object_id = sed.referenced_id + AND c.column_id = sed.referenced_minor_id + WHERE sed.referencing_class = 7 + AND sed.referenced_class = 1 + AND i.has_filter = 1 + AND NOT EXISTS ( SELECT 1/0 + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns AS ic + WHERE ic.index_id = sed.referencing_minor_id + AND ic.column_id = sed.referenced_minor_id + AND ic.object_id = sed.referenced_id ) + OPTION(RECOMPILE);' + + BEGIN TRY + INSERT #FilteredIndexes ( database_id, database_name, schema_name, table_name, index_name, column_name ) + EXEC sp_executesql @dsql, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; + END TRY + 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; @@ -1493,7 +2758,7 @@ BEGIN CATCH RAISERROR(@msg, 0, 1) WITH NOWAIT; END; - SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + SELECT @msg = @DatabaseName + N' database failed to process. ' + ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); RAISERROR (@msg,@ErrorSeverity, @ErrorState )WITH NOWAIT; @@ -1520,8 +2785,16 @@ RAISERROR (N'Updating #IndexSanity.key_column_names',0,1) WITH NOWAIT; UPDATE #IndexSanity SET key_column_names = D1.key_column_names FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' AS col_definition FROM #IndexColumns c WHERE c.database_id= si.database_id @@ -1531,14 +2804,14 @@ FROM #IndexSanity si AND c.is_included_column = 0 /*Just Keys*/ AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') ,TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH('') ,TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) ) D1 ( key_column_names ); RAISERROR (N'Updating #IndexSanity.partition_key_column_name',0,1) WITH NOWAIT; UPDATE #IndexSanity SET partition_key_column_name = D1.partition_key_column_name FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition FROM #IndexColumns c WHERE c.database_id= si.database_id AND c.schema_name = si.schema_name @@ -1546,18 +2819,27 @@ FROM #IndexSanity si AND c.index_id = si.index_id AND c.partition_ordinal <> 0 /*Just Partitioned Keys*/ ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1,''))) D1 + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 ( partition_key_column_name ); RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order',0,1) WITH NOWAIT; UPDATE #IndexSanity SET key_column_names_with_sort_order = D2.key_column_names_with_sort_order FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + CASE c.is_descending_key WHEN 1 THEN N' DESC' ELSE N'' - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' - END AS col_definition + END + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' + AS col_definition FROM #IndexColumns c WHERE c.database_id= si.database_id AND c.schema_name = si.schema_name @@ -1566,14 +2848,14 @@ FROM #IndexSanity si AND c.is_included_column = 0 /*Just Keys*/ AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) ) D2 ( key_column_names_with_sort_order ); RAISERROR (N'Updating #IndexSanity.key_column_names_with_sort_order_no_types (for create tsql)',0,1) WITH NOWAIT; UPDATE #IndexSanity SET key_column_names_with_sort_order_no_types = D2.key_column_names_with_sort_order_no_types FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CASE c.is_descending_key WHEN 1 THEN N' DESC' ELSE N'' END AS col_definition @@ -1585,15 +2867,23 @@ FROM #IndexSanity si AND c.is_included_column = 0 /*Just Keys*/ AND c.key_ordinal > 0 /*Ignore non-key columns, such as partitioning keys*/ ORDER BY c.object_id, c.index_id, c.key_ordinal - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) ) D2 ( key_column_names_with_sort_order_no_types ); RAISERROR (N'Updating #IndexSanity.include_column_names',0,1) WITH NOWAIT; UPDATE #IndexSanity SET include_column_names = D3.include_column_names FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name - + N' {' + system_type_name + N' ' + CAST(max_length AS NVARCHAR(50)) + N'}' + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name + + N' {' + system_type_name + + CASE max_length WHEN -1 THEN N' (max)' ELSE + CASE + WHEN system_type_name IN (N'char',N'varchar',N'binary',N'varbinary') THEN N' (' + CAST(max_length AS NVARCHAR(20)) + N')' + WHEN system_type_name IN (N'nchar',N'nvarchar') THEN N' (' + CAST(max_length/2 AS NVARCHAR(20)) + N')' + ELSE N' ' + CAST(max_length AS NVARCHAR(50)) + END + END + + N'}' FROM #IndexColumns c WHERE c.database_id= si.database_id AND c.schema_name = si.schema_name @@ -1602,14 +2892,14 @@ FROM #IndexSanity si AND c.is_included_column = 1 /*Just includes*/ ORDER BY c.column_name /*Order doesn't matter in includes, this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) ) D3 ( include_column_names ); RAISERROR (N'Updating #IndexSanity.include_column_names_no_types (for create tsql)',0,1) WITH NOWAIT; UPDATE #IndexSanity SET include_column_names_no_types = D3.include_column_names_no_types FROM #IndexSanity si - CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + QUOTENAME(c.column_name) FROM #IndexColumns c WHERE c.database_id= si.database_id AND c.schema_name = si.schema_name @@ -1618,7 +2908,7 @@ FROM #IndexSanity si AND c.is_included_column = 1 /*Just includes*/ ORDER BY c.column_name /*Order doesn't matter in includes, this is here to make rows easy to compare.*/ - FOR XML PATH('') , TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) ) D3 ( include_column_names_no_types ); RAISERROR (N'Updating #IndexSanity.count_key_columns and count_include_columns',0,1) WITH NOWAIT; @@ -1626,7 +2916,7 @@ UPDATE #IndexSanity SET count_included_columns = D4.count_included_columns, count_key_columns = D4.count_key_columns FROM #IndexSanity si - CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 + CROSS APPLY ( SELECT SUM(CASE WHEN is_included_column = 'true' THEN 1 ELSE 0 END) AS count_included_columns, SUM(CASE WHEN is_included_column = 'false' AND c.key_ordinal > 0 THEN 1 @@ -1650,17 +2940,20 @@ FROM #IndexPartitionSanity ps RAISERROR (N'Inserting data into #IndexSanitySize',0,1) WITH NOWAIT; -INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], partition_count, total_rows, total_reserved_MB, - total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_range_scan_count, +INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], [lock_escalation_desc], partition_count, total_rows, total_reserved_MB, + total_reserved_LOB_MB, total_reserved_row_overflow_MB, total_reserved_dictionary_MB, total_range_scan_count, total_singleton_lookup_count, total_leaf_delete_count, total_leaf_update_count, total_forwarded_fetch_count,total_row_lock_count, total_row_lock_wait_count, total_row_lock_wait_in_ms, avg_row_lock_wait_in_ms, total_page_lock_count, total_page_lock_wait_count, total_page_lock_wait_in_ms, avg_page_lock_wait_in_ms, total_index_lock_promotion_attempt_count, - total_index_lock_promotion_count, data_compression_desc ) - SELECT index_sanity_id, ipp.database_id, ipp.schema_name, - COUNT(*), SUM(row_count), SUM(reserved_MB), SUM(reserved_LOB_MB), + total_index_lock_promotion_count, data_compression_desc, + page_latch_wait_count, page_latch_wait_in_ms, page_io_latch_wait_count, page_io_latch_wait_in_ms) + SELECT index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc, + COUNT(*), SUM(row_count), SUM(reserved_MB), + SUM(reserved_LOB_MB) - SUM(reserved_dictionary_MB), /* Subtract columnstore dictionaries from LOB data */ SUM(reserved_row_overflow_MB), + SUM(reserved_dictionary_MB), SUM(range_scan_count), SUM(singleton_lookup_count), SUM(leaf_delete_count), @@ -1680,29 +2973,34 @@ INSERT #IndexSanitySize ( [index_sanity_id], [database_id], [schema_name], pa ELSE 0 END AS avg_page_lock_wait_in_ms, SUM(index_lock_promotion_attempt_count), SUM(index_lock_promotion_count), - LEFT(MAX(data_compression_info.data_compression_rollup),8000) + LEFT(MAX(data_compression_info.data_compression_rollup),4000), + SUM(page_latch_wait_count), + SUM(page_latch_wait_in_ms), + SUM(page_io_latch_wait_count), + SUM(page_io_latch_wait_in_ms) FROM #IndexPartitionSanity ipp /* individual partitions can have distinct compression settings, just roll them into a list here*/ OUTER APPLY (SELECT STUFF(( - SELECT N', ' + data_compression_desc + SELECT N', ' + data_compression_desc FROM #IndexPartitionSanity ipp2 WHERE ipp.[object_id]=ipp2.[object_id] AND ipp.[index_id]=ipp2.[index_id] AND ipp.database_id = ipp2.database_id AND ipp.schema_name = ipp2.schema_name ORDER BY ipp2.partition_number - FOR XML PATH(''),TYPE).value('.', 'varchar(max)'), 1, 1, '')) + FOR XML PATH(''),TYPE).value('.', 'nvarchar(max)'), 1, 1, '')) data_compression_info(data_compression_rollup) - GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name + GROUP BY index_sanity_id, ipp.database_id, ipp.schema_name, ipp.lock_escalation_desc ORDER BY index_sanity_id OPTION ( RECOMPILE ); RAISERROR (N'Determining index usefulness',0,1) WITH NOWAIT; UPDATE #MissingIndexes -SET is_low = CASE WHEN (user_seeks + user_scans) < 10000 - OR avg_user_impact < 70. THEN 1 - ELSE 0 - END; +SET is_low = CASE WHEN (user_seeks + user_scans) < 5000 + OR unique_compiles = 1 + THEN 1 + ELSE 0 + END; RAISERROR (N'Updating #IndexSanity.referenced_by_foreign_key',0,1) WITH NOWAIT; UPDATE #IndexSanity @@ -1717,7 +3015,7 @@ RAISERROR (N'Update index_secret on #IndexSanity for NC indexes.',0,1) WITH NOWA UPDATE nc SET secret_columns= N'[' + - CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS VARCHAR(10)) END + + CASE tb.count_key_columns WHEN 0 THEN '1' ELSE CAST(tb.count_key_columns AS NVARCHAR(10)) END + CASE nc.is_unique WHEN 1 THEN N' INCLUDE' ELSE N' KEY' END + CASE WHEN tb.count_key_columns > 1 THEN N'S] ' ELSE N'] ' END + CASE tb.index_id WHEN 0 THEN '[RID]' ELSE LTRIM(tb.key_column_names) + @@ -1750,94 +3048,112 @@ INSERT #IndexCreateTsql (index_sanity_id, create_tsql) SELECT index_sanity_id, ISNULL ( - /* Script drops for disabled non-clustered indexes*/ - CASE WHEN is_disabled = 1 AND index_id <> 1 - THEN N'--DROP INDEX ' + QUOTENAME([index_name]) + N' ON ' - + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) - ELSE - CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' + CASE index_id WHEN 0 THEN N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ' REBUILD;' + ELSE + CASE WHEN is_XML = 1 OR is_spatial = 1 OR is_in_memory_oltp = 1 THEN N'' /* Not even trying for these just yet...*/ ELSE - CASE WHEN is_XML = 1 OR is_spatial=1 THEN N'' /* Not even trying for these just yet...*/ - ELSE - CASE WHEN is_primary_key=1 THEN - N'ALTER TABLE ' + QUOTENAME([schema_name]) + - N'.' + QUOTENAME([object_name]) + - N' ADD CONSTRAINT [' + - index_name + - N'] PRIMARY KEY ' + - CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + - key_column_names_with_sort_order_no_types + N' )' - WHEN is_CX_columnstore= 1 THEN - N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) - ELSE /*Else not a PK or cx columnstore */ - N'CREATE ' + - CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + - CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + - CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' - ELSE N'' END + - N'INDEX [' - + index_name + N'] ON ' + - QUOTENAME([schema_name]) + '.' + QUOTENAME([object_name]) + - CASE WHEN is_NC_columnstore=1 THEN - N' (' + ISNULL(include_column_names_no_types,'') + N' )' - ELSE /*Else not colunnstore */ - N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' - + CASE WHEN include_column_names_no_types IS NOT NULL THEN - N' INCLUDE (' + include_column_names_no_types + N')' - ELSE N'' - END - END /*End non-colunnstore case */ - + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END - END /*End Non-PK index CASE */ - + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN - N' WITH (' - + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' - + N'ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' - + N')' - ELSE N'' END - + N';' - END /*End non-spatial and non-xml CASE */ - END + CASE WHEN is_primary_key=1 THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] PRIMARY KEY ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_unique_constraint = 1 AND is_primary_key = 0 + THEN + N'ALTER TABLE ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + + N'.' + QUOTENAME([object_name]) + + N' ADD CONSTRAINT [' + + index_name + + N'] UNIQUE ' + + CASE WHEN index_id=1 THEN N'CLUSTERED (' ELSE N'(' END + + key_column_names_with_sort_order_no_types + N' )' + WHEN is_CX_columnstore= 1 THEN + N'CREATE CLUSTERED COLUMNSTORE INDEX ' + QUOTENAME(index_name) + N' on ' + QUOTENAME([database_name]) + N'.' + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + ELSE /*Else not a PK or cx columnstore */ + N'CREATE ' + + CASE WHEN is_unique=1 THEN N'UNIQUE ' ELSE N'' END + + CASE WHEN index_id=1 THEN N'CLUSTERED ' ELSE N'' END + + CASE WHEN is_NC_columnstore=1 THEN N'NONCLUSTERED COLUMNSTORE ' + ELSE N'' END + + N'INDEX [' + + index_name + N'] ON ' + + QUOTENAME([database_name]) + N'.' + + QUOTENAME([schema_name]) + N'.' + QUOTENAME([object_name]) + + CASE WHEN is_NC_columnstore=1 THEN + N' (' + ISNULL(include_column_names_no_types,'') + N' )' + ELSE /*Else not columnstore */ + N' (' + ISNULL(key_column_names_with_sort_order_no_types,'') + N' )' + + CASE WHEN include_column_names_no_types IS NOT NULL THEN + N' INCLUDE (' + include_column_names_no_types + N')' + ELSE N'' + END + END /*End non-columnstore case */ + + CASE WHEN filter_definition <> N'' THEN N' WHERE ' + filter_definition ELSE N'' END + END /*End Non-PK index CASE */ + + CASE WHEN is_NC_columnstore=0 AND is_CX_columnstore=0 THEN + N' WITH (' + + N'FILLFACTOR=' + CASE fill_factor WHEN 0 THEN N'100' ELSE CAST(fill_factor AS NVARCHAR(5)) END + ', ' + + N'ONLINE=?, SORT_IN_TEMPDB=?, DATA_COMPRESSION=?' + + N')' + ELSE N'' END + + N';' + END /*End non-spatial and non-xml CASE */ END, '[Unknown Error]') AS create_tsql FROM #IndexSanity; RAISERROR (N'Populate #PartitionCompressionInfo.',0,1) WITH NOWAIT; -;WITH [maps] - AS ( SELECT - index_sanity_id, - partition_number, - data_compression_desc, - partition_number - ROW_NUMBER() OVER (PARTITION BY ips.index_sanity_id, data_compression_desc ORDER BY partition_number ) AS [rN] - FROM #IndexPartitionSanity ips - ), - [grps] - AS ( SELECT MIN([maps].[partition_number]) AS [MinKey] , - MAX([maps].[partition_number]) AS [MaxKey] , - index_sanity_id, - maps.data_compression_desc - FROM [maps] - GROUP BY [maps].[rN], index_sanity_id, maps.data_compression_desc) -INSERT #PartitionCompressionInfo - (index_sanity_id, partition_compression_detail) -SELECT DISTINCT grps.index_sanity_id , SUBSTRING(( STUFF((SELECT ', ' + ' Partition' - + CASE WHEN [grps2].[MinKey] < [grps2].[MaxKey] - THEN +'s ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' - ' - + CAST([grps2].[MaxKey] AS VARCHAR) - + ' use ' + grps2.data_compression_desc - ELSE ' ' - + CAST([grps2].[MinKey] AS VARCHAR) - + ' uses ' + grps2.data_compression_desc - END AS [Partitions] - FROM [grps] AS grps2 - WHERE grps2.index_sanity_id = grps.index_sanity_id - ORDER BY grps2.MinKey, grps2.MaxKey - FOR XML PATH('') , - TYPE - ).[value]('.', 'VARCHAR(MAX)'), 1, 1, '') ), 0, 8000) AS [partition_compression_detail] -FROM grps; +IF OBJECT_ID('tempdb..#maps') IS NOT NULL DROP TABLE #maps; +WITH maps + AS + ( + SELECT ips.index_sanity_id, + ips.partition_number, + ips.data_compression_desc, + ips.partition_number - ROW_NUMBER() OVER ( PARTITION BY ips.index_sanity_id, ips.data_compression_desc + ORDER BY ips.partition_number ) AS rn + FROM #IndexPartitionSanity AS ips + ) +SELECT * +INTO #maps +FROM maps; + +IF OBJECT_ID('tempdb..#grps') IS NOT NULL DROP TABLE #grps; +WITH grps + AS + ( + SELECT MIN(maps.partition_number) AS MinKey, + MAX(maps.partition_number) AS MaxKey, + maps.index_sanity_id, + maps.data_compression_desc + FROM #maps AS maps + GROUP BY maps.rn, maps.index_sanity_id, maps.data_compression_desc + ) +SELECT * +INTO #grps +FROM grps; + +INSERT #PartitionCompressionInfo ( index_sanity_id, partition_compression_detail ) +SELECT DISTINCT + grps.index_sanity_id, + SUBSTRING( + ( STUFF( + ( SELECT N', ' + N' Partition' + + CASE + WHEN grps2.MinKey < grps2.MaxKey + THEN + + N's ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' - ' + + CAST(grps2.MaxKey AS NVARCHAR(10)) + N' use ' + grps2.data_compression_desc + ELSE + N' ' + CAST(grps2.MinKey AS NVARCHAR(10)) + N' uses ' + grps2.data_compression_desc + END AS Partitions + FROM #grps AS grps2 + WHERE grps2.index_sanity_id = grps.index_sanity_id + ORDER BY grps2.MinKey, grps2.MaxKey + FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)'), 1, 1, '')), 0, 8000) AS partition_compression_detail +FROM #grps AS grps; RAISERROR (N'Update #PartitionCompressionInfo.',0,1) WITH NOWAIT; UPDATE sz @@ -1846,25 +3162,44 @@ FROM #IndexSanitySize sz JOIN #PartitionCompressionInfo AS pci ON pci.index_sanity_id = sz.index_sanity_id; +RAISERROR (N'Update #IndexSanity for filtered indexes with columns not in the index definition.',0,1) WITH NOWAIT; +UPDATE #IndexSanity +SET filter_columns_not_in_index = D1.filter_columns_not_in_index +FROM #IndexSanity si + CROSS APPLY ( SELECT RTRIM(STUFF( (SELECT N', ' + c.column_name AS col_definition + FROM #FilteredIndexes AS c + WHERE c.database_id= si.database_id + AND c.schema_name = si.schema_name + AND c.table_name = si.object_name + AND c.index_name = si.index_name + ORDER BY c.index_sanity_id + FOR XML PATH('') , TYPE).value('.', 'nvarchar(max)'), 1, 1,''))) D1 + ( filter_columns_not_in_index ); + + +IF @Debug = 1 +BEGIN + SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults AS bir; + SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; + SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; + SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; + SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; + SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; + SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; + SELECT '#UnindexedForeignKeys' AS table_name, * FROM #UnindexedForeignKeys; + SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; + SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; + 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 '#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; +END -/*This is for debugging*/ ---SELECT '#IndexSanity' AS table_name, * FROM #IndexSanity; ---SELECT '#IndexPartitionSanity' AS table_name, * FROM #IndexPartitionSanity; ---SELECT '#IndexSanitySize' AS table_name, * FROM #IndexSanitySize; ---SELECT '#IndexColumns' AS table_name, * FROM #IndexColumns; ---SELECT '#MissingIndexes' AS table_name, * FROM #MissingIndexes; ---SELECT '#ForeignKeys' AS table_name, * FROM #ForeignKeys; ---SELECT '#BlitzIndexResults' AS table_name, * FROM #BlitzIndexResults; ---SELECT '#IndexCreateTsql' AS table_name, * FROM #IndexCreateTsql; ---SELECT '#DatabaseList' AS table_name, * FROM #DatabaseList; ---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; -/*End debug*/ - - ---------------------------------------- --STEP 3: DIAGNOSE THE PATIENT ---------------------------------------- @@ -1882,7 +3217,8 @@ BEGIN --We do a left join here in case this is a disabled NC. --In that case, it won't have any size info/pages allocated. - + IF (@ShowColumnstoreOnly = 0) + BEGIN WITH table_mode_cte AS ( SELECT s.db_schema_object_indexid, @@ -1905,7 +3241,23 @@ BEGIN s.last_user_update, s.create_date, s.modify_date, + sz.page_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_latch_wait_in_ms / 1000), 0), 108) AS page_latch_wait_time, + sz.page_io_latch_wait_count, + CONVERT(VARCHAR(10), (sz.page_io_latch_wait_in_ms / 1000) / 86400) + ':' + CONVERT(VARCHAR(20), DATEADD(s, (sz.page_io_latch_wait_in_ms / 1000), 0), 108) AS page_io_latch_wait_time, ct.create_tsql, + CASE + WHEN s.is_primary_key = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND is_unique_constraint = 1 AND s.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(s.[database_name]) + N'.' + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(s.index_name) + N';' + WHEN s.is_primary_key = 0 AND s.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(s.index_name) + N' ON ' + QUOTENAME(s.[database_name]) + N'.' + + QUOTENAME(s.[schema_name]) + N'.' + QUOTENAME(s.[object_name]) + N';' + ELSE N'' + END AS drop_tsql, 1 AS display_order FROM #IndexSanity s LEFT JOIN #IndexSanitySize sz ON @@ -1916,12 +3268,12 @@ BEGIN pci.index_sanity_id = s.index_sanity_id WHERE s.[object_id]=@ObjectID UNION ALL - SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + + SELECT N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) + N' (' + @ScriptVersionName + ')' , N'SQL Server First Responder Kit' , N'http://FirstResponderKit.org' , N'From Your Community Volunteers', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,@DaysUptimeInsertValue,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, 0 AS display_order ) SELECT @@ -1942,13 +3294,18 @@ BEGIN last_user_update AS [Last User Write], create_date AS [Created], modify_date AS [Last Modified], - create_tsql AS [Create TSQL] + page_latch_wait_count AS [Page Latch Wait Count], + page_latch_wait_time as [Page Latch Wait Time (D:H:M:S)], + page_io_latch_wait_count AS [Page IO Latch Wait Count], + page_io_latch_wait_time as [Page IO Latch Wait Time (D:H:M:S)], + create_tsql AS [Create TSQL], + drop_tsql AS [Drop TSQL] FROM table_mode_cte ORDER BY display_order ASC, key_column_names ASC OPTION ( RECOMPILE ); IF (SELECT TOP 1 [object_id] FROM #MissingIndexes mi) IS NOT NULL - BEGIN + BEGIN; WITH create_date AS ( SELECT i.database_id, @@ -1959,7 +3316,7 @@ BEGIN GROUP BY i.database_id, i.schema_name, i.object_id ) SELECT N'Missing index.' AS Finding , - N'http://BrentOzar.com/go/Indexaphobia' AS URL , + N'https://www.brentozar.com/go/Indexaphobia' AS URL , mi.[statement] + ' Est. Benefit: ' + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' @@ -1969,22 +3326,24 @@ BEGIN END AS [Estimated Benefit], missing_index_details AS [Missing Index Request] , index_estimated_impact AS [Estimated Impact], - create_tsql AS [Create TSQL] + create_tsql AS [Create TSQL], + sample_query_plan AS [Sample Query Plan] FROM #MissingIndexes mi LEFT JOIN create_date AS cd ON mi.[object_id] = cd.object_id AND mi.database_id = cd.database_id AND mi.schema_name = cd.schema_name WHERE mi.[object_id] = @ObjectID + AND (@ShowAllMissingIndexRequests=1 /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - AND (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - ORDER BY is_low, magic_benefit_number DESC + OR (magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000) + ORDER BY magic_benefit_number DESC OPTION ( RECOMPILE ); END; ELSE SELECT 'No missing indexes.' AS finding; - SELECT + SELECT column_name AS [Column Name], (SELECT COUNT(*) FROM #IndexColumns c2 @@ -2035,49 +3394,398 @@ BEGIN END; ELSE SELECT 'No foreign keys.' AS finding; -END; ---If @TableName is NOT specified... ---Act based on the @Mode and @Filter. (@Filter applies only when @Mode=0 "diagnose") -ELSE -BEGIN; - IF @Mode IN (0, 4) /* DIAGNOSE*/ - BEGIN; - RAISERROR(N'@Mode=0 or 4, we are diagnosing.', 0,1) WITH NOWAIT; + /* Show histograms for all stats on this table. More info: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1900 */ + IF EXISTS (SELECT * FROM sys.all_objects WHERE name = 'dm_db_stats_histogram') + BEGIN + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT s.name AS [Stat Name], c.name AS [Leading Column Name], hist.step_number AS [Step Number], + hist.range_high_key AS [Range High Key], hist.range_rows AS [Range Rows], + hist.equal_rows AS [Equal Rows], hist.distinct_range_rows AS [Distinct Range Rows], hist.average_range_rows AS [Average Range Rows], + s.auto_created AS [Auto-Created], s.user_created AS [User-Created], + props.last_updated AS [Last Updated], props.modification_counter AS [Modification Counter], props.rows AS [Table Rows], + props.rows_sampled AS [Rows Sampled], s.stats_id AS [StatsID] + FROM sys.stats AS s + INNER JOIN sys.stats_columns sc ON s.object_id = sc.object_id AND s.stats_id = sc.stats_id AND sc.stats_column_id = 1 + INNER JOIN sys.columns c ON sc.object_id = c.object_id AND sc.column_id = c.column_id + CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS props + CROSS APPLY sys.dm_db_stats_histogram(s.[object_id], s.stats_id) AS hist + WHERE s.object_id = @ObjectID + ORDER BY s.auto_created, s.user_created, s.name, hist.step_number;'; + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + 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')) + BEGIN + RAISERROR(N'Visualizing columnstore index contents.', 0,1) WITH NOWAIT; + + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + IF EXISTS(SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = @ObjectID) + BEGIN + SELECT @ColumnList = N'''', @ColumnListWithApostrophes = N''''; + WITH DistinctColumns AS ( + SELECT DISTINCT QUOTENAME(c.name) AS column_name, QUOTENAME(c.name,'''''''') AS ColumnNameWithApostrophes, c.column_id + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON p.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id and ic.object_id = c.object_id AND ic.index_id = p.index_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.types t ON c.system_type_id = t.system_type_id AND c.user_type_id = t.user_type_id + WHERE p.object_id = @ObjectID + AND EXISTS (SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg WHERE p.partition_id = seg.partition_id AND seg.column_id = ic.index_column_id) + AND p.data_compression IN (3,4) + ) + SELECT @ColumnList = @ColumnList + column_name + N'', '', + @ColumnListWithApostrophes = @ColumnListWithApostrophes + ColumnNameWithApostrophes + N'', '' + FROM DistinctColumns + ORDER BY column_id; + SELECT @PartitionCount = COUNT(1) FROM ' + QUOTENAME(@DatabaseName) + N'.sys.partitions WHERE object_id = @ObjectID AND data_compression IN (3,4); + END'; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + EXEC sp_executesql @dsql, N'@ObjectID INT, @ColumnList NVARCHAR(MAX) OUTPUT, @ColumnListWithApostrophes NVARCHAR(MAX) OUTPUT, @PartitionCount INT OUTPUT', @ObjectID, @ColumnList OUTPUT, @ColumnListWithApostrophes OUTPUT, @PartitionCount OUTPUT; + + IF @PartitionCount < 2 + SET @ShowPartitionRanges = 0; + + IF @Debug = 1 + SELECT @ColumnList AS ColumnstoreColumnList, @ColumnListWithApostrophes AS ColumnstoreColumnListWithApostrophes, @PartitionCount AS PartitionCount, @ShowPartitionRanges AS ShowPartitionRanges; + + IF @ColumnList <> '' + BEGIN + /* Remove the trailing comma */ + SET @ColumnList = LEFT(@ColumnList, LEN(@ColumnList) - 1); + SET @ColumnListWithApostrophes = LEFT(@ColumnListWithApostrophes, LEN(@ColumnListWithApostrophes) - 1); + + SET @dsql = N'USE ' + QUOTENAME(@DatabaseName) + N'; + SELECT partition_number, ' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' COALESCE(range_start_op + '' '' + range_start + '' '', '''') + COALESCE(range_end_op + '' '' + range_end, '''') AS partition_range, ' ELSE N' ' END + + N' row_group_id, total_rows, deleted_rows, ' + + @ColumnList + + CASE WHEN @ShowPartitionRanges = 1 THEN N' , + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT column_name, partition_number, row_group_id, total_rows, deleted_rows, details, + range_start_op, + CASE + WHEN format_type IS NULL THEN CAST(range_start_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_start_value, format_type) END range_start, + range_end_op, + CASE + WHEN format_type IS NULL THEN CAST(range_end_value AS NVARCHAR(4000)) + ELSE CONVERT(NVARCHAR(4000), range_end_value, format_type) END range_end' ELSE N' ' END + N', + state_desc, trim_reason_desc, transition_to_compressed_state_desc, has_vertipaq_optimization + FROM ( + SELECT c.name AS column_name, p.partition_number, rg.row_group_id, rg.total_rows, rg.deleted_rows, + phys.state_desc, phys.trim_reason_desc, phys.transition_to_compressed_state_desc, phys.has_vertipaq_optimization, + details = CAST(seg.min_data_id AS VARCHAR(20)) + '' to '' + CAST(seg.max_data_id AS VARCHAR(20)) + '', '' + CAST(CAST(((COALESCE(d.on_disk_size,0) + COALESCE(seg.on_disk_size,0)) / 1024.0 / 1024) AS DECIMAL(18,0)) AS VARCHAR(20)) + '' MB''' + + CASE WHEN @ShowPartitionRanges = 1 THEN N', + CASE + WHEN pp.system_type_id IN (40, 41, 42, 43, 58, 61) THEN 126 + WHEN pp.system_type_id IN (59, 62) THEN 3 + WHEN pp.system_type_id IN (60, 122) THEN 2 + ELSE NULL END format_type, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''>'' ELSE ''>='' END range_start_op, + prvs.value range_start_value, + CASE WHEN pf.boundary_value_on_right = 0 THEN ''<='' ELSE ''<'' END range_end_op, + prve.value range_end_value ' ELSE N' ' END + N' + FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups rg + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.columns c ON rg.object_id = c.object_id + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partitions p ON rg.object_id = p.object_id AND rg.partition_number = p.partition_number + INNER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.index_columns ic on ic.column_id = c.column_id AND ic.object_id = c.object_id AND ic.index_id = p.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.dm_db_column_store_row_group_physical_stats phys ON rg.row_group_id = phys.row_group_id AND rg.object_id = phys.object_id AND rg.partition_number = phys.partition_number AND rg.index_id = phys.index_id ' + CASE WHEN @ShowPartitionRanges = 1 THEN N' + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.indexes i ON i.object_id = rg.object_id AND i.index_id = rg.index_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_schemes ps ON ps.data_space_id = i.data_space_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_functions pf ON pf.function_id = ps.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_parameters pp ON pp.function_id = pf.function_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prvs ON prvs.function_id = pf.function_id AND prvs.boundary_id = p.partition_number - 1 + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.partition_range_values prve ON prve.function_id = pf.function_id AND prve.boundary_id = p.partition_number ' ELSE N' ' END + + N' LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_segments seg ON p.partition_id = seg.partition_id AND ic.index_column_id = seg.column_id AND rg.row_group_id = seg.segment_id + LEFT OUTER JOIN ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_dictionaries d ON p.hobt_id = d.hobt_id AND c.column_id = d.column_id AND seg.secondary_dictionary_id = d.dictionary_id + WHERE rg.object_id = @ObjectID + AND rg.state IN (1, 2, 3) + AND c.name IN ( ' + @ColumnListWithApostrophes + N')' + + CASE WHEN @ShowPartitionRanges = 1 THEN N' + ) AS y ' ELSE N' ' END + N' + ) AS x + PIVOT (MAX(details) FOR column_name IN ( ' + @ColumnList + N')) AS pivot1 + ORDER BY partition_number, row_group_id;'; + + IF @Debug = 1 + BEGIN + PRINT SUBSTRING(@dsql, 0, 4000); + PRINT SUBSTRING(@dsql, 4000, 8000); + PRINT SUBSTRING(@dsql, 8000, 12000); + PRINT SUBSTRING(@dsql, 12000, 16000); + PRINT SUBSTRING(@dsql, 16000, 20000); + PRINT SUBSTRING(@dsql, 20000, 24000); + PRINT SUBSTRING(@dsql, 24000, 28000); + PRINT SUBSTRING(@dsql, 28000, 32000); + PRINT SUBSTRING(@dsql, 32000, 36000); + PRINT SUBSTRING(@dsql, 36000, 40000); + END; + + IF @dsql IS NULL + RAISERROR('@dsql is null',16,1); + ELSE + EXEC sp_executesql @dsql, N'@ObjectID INT', @ObjectID; + END + ELSE /* No columns were found for this object */ + BEGIN + SELECT N'No compressed columnstore rowgroups were found for this object.' AS Columnstore_Visualization + UNION ALL + SELECT N'SELECT * FROM ' + QUOTENAME(@DatabaseName) + N'.sys.column_store_row_groups WHERE object_id = ' + CAST(@ObjectID AS NVARCHAR(100)); + END + RAISERROR(N'Done visualizing columnstore index contents.', 0,1) WITH NOWAIT; + END + + IF @ShowColumnstoreOnly = 1 + RETURN; + +END; /* IF @TableName IS NOT NULL */ + + + + + + + + +ELSE /* @TableName IS NULL, so we operate in normal mode 0/1/2/3/4 */ +BEGIN + +/* Validate and check table output params */ + + + /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ + DECLARE @ValidOutputServer BIT; + DECLARE @ValidOutputLocation BIT; + DECLARE @LinkedServerDBCheck NVARCHAR(2000); + DECLARE @ValidLinkedServerDB INT; + DECLARE @tmpdbchk TABLE (cnt INT); + + IF @OutputServerName IS NOT NULL + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); + END; + ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + BEGIN + SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; + INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; + SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); + IF (@ValidLinkedServerDB > 0) + BEGIN + SET @ValidOutputServer = 1; + SET @ValidOutputLocation = 1; + END; + ELSE + RAISERROR('The specified database was not found on the output server', 16, 0); + END; + ELSE + BEGIN + RAISERROR('The specified output server was not found', 16, 0); + END; + END; + ELSE + BEGIN + IF (SUBSTRING(@OutputTableName, 2, 2) = '##') + BEGIN + SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + EXEC(@StringToExecute); + + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + SET @OutputDatabaseName = '[tempdb]'; + SET @OutputSchemaName = '[dbo]'; + SET @ValidOutputLocation = 1; + END; + ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + BEGIN + RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ValidOutputLocation = 1; + SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + END; + ELSE IF @OutputDatabaseName IS NOT NULL + AND @OutputSchemaName IS NOT NULL + AND @OutputTableName IS NOT NULL + AND NOT EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + RAISERROR('The specified output database was not found on this server', 16, 0); + END; + ELSE + BEGIN + SET @ValidOutputLocation = 0; + END; + END; + + IF (@ValidOutputLocation = 0 AND @OutputType = 'NONE') + BEGIN + RAISERROR('Invalid output location and no output asked',12,1); + RETURN; + END; + + /* @OutputTableName lets us export the results to a permanent table */ + DECLARE @RunID UNIQUEIDENTIFIER; + SET @RunID = NEWID(); + + DECLARE @TableExists BIT; + DECLARE @SchemaExists BIT; + + DECLARE @TableExistsSql NVARCHAR(MAX); + + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + SET @StringToExecute = + N'SET @SchemaExists = 0; + SET @TableExists = 0; + IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + SET @SchemaExists = 1 + IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + BEGIN + SET @TableExists = 1 + IF NOT EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.COLUMNS WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' + AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'' AND QUOTENAME(COLUMN_NAME) = ''[total_forwarded_fetch_count]'') + EXEC @@@OutputServerName@@@.@@@OutputDatabaseName@@@.dbo.sp_executesql N''ALTER TABLE @@@OutputSchemaName@@@.@@@OutputTableName@@@ ADD [total_forwarded_fetch_count] BIGINT'' + END'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + + EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT; + + + SET @TableExistsSql = + N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') + AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') + SET @TableExists = 0 + ELSE + SET @TableExists = 1'; + + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputServerName@@@', @OutputServerName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @TableExistsSql = REPLACE(@TableExistsSql, '@@@OutputTableName@@@', @OutputTableName); + + END + - ---------------------------------------- - --Multiple Index Personalities: Check_id 0-10 - ---------------------------------------- - BEGIN; - --SELECT [object_id], key_column_names, database_id - -- FROM #IndexSanity - -- WHERE index_type IN (1,2) /* Clustered, NC only*/ - -- AND is_hypothetical = 0 - -- AND is_disabled = 0 - -- GROUP BY [object_id], key_column_names, database_id - -- HAVING COUNT(*) > 1 + IF @Mode IN (0, 4) /* DIAGNOSE */ + BEGIN; + IF @Mode IN (0, 4) /* DIAGNOSE priorities 1-100 */ + BEGIN; + RAISERROR(N'@Mode=0 or 4, running rules for priorities 1-100.', 0,1) WITH NOWAIT; + ---------------------------------------- + --Multiple Index Personalities: Check_id 0-10 + ---------------------------------------- RAISERROR('check_id 1: Duplicate keys', 0,1) WITH NOWAIT; WITH duplicate_indexes - AS ( SELECT [object_id], key_column_names, database_id, [schema_name] - FROM #IndexSanity + AS ( SELECT [object_id], key_column_names, database_id, [schema_name] + FROM #IndexSanity AS ip WHERE index_type IN (1,2) /* Clustered, NC only*/ AND is_hypothetical = 0 AND is_disabled = 0 AND is_primary_key = 0 + AND EXISTS ( + SELECT 1/0 + FROM #IndexSanitySize ips + WHERE ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name + AND ips.total_reserved_MB >= CASE + WHEN (@GetAllDatabases = 1 OR @Mode = 0) + THEN @ThresholdMB + ELSE ips.total_reserved_MB + END + ) GROUP BY [object_id], key_column_names, database_id, [schema_name] HAVING COUNT(*) > 1) 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 1 AS check_id, + SELECT 1 AS check_id, ip.index_sanity_id, - 50 AS Priority, - 'Multiple Index Personalities' AS findings_group, + 20 AS Priority, + 'Redundant Indexes' AS findings_group, 'Duplicate keys' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, + N'https://www.brentozar.com/go/duplicateindex' AS URL, N'Index Name: ' + ip.index_name + N' Table Name: ' + ip.db_schema_object_name AS details, ip.index_definition, ip.secret_columns, @@ -2088,11 +3796,13 @@ BEGIN; AND ip.database_id = di.database_id AND ip.[schema_name] = di.[schema_name] AND di.key_column_names = ip.key_column_names - JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id AND ip.database_id = ips.database_id + JOIN #IndexSanitySize ips ON ip.index_sanity_id = ips.index_sanity_id + AND ip.database_id = ips.database_id + AND ip.schema_name = ips.schema_name /* WHERE clause limits to only @ThresholdMB or larger duplicate indexes when getting all databases or using PainRelief mode */ WHERE ips.total_reserved_MB >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN @ThresholdMB ELSE ips.total_reserved_MB END AND ip.is_primary_key = 0 - ORDER BY ip.object_id, ip.key_column_names_with_sort_order + ORDER BY ips.total_rows DESC, ip.[schema_name], ip.[object_name], ip.key_column_names_with_sort_order OPTION ( RECOMPILE ); RAISERROR('check_id 2: Keys w/ identical leading columns.', 0,1) WITH NOWAIT; @@ -2106,13 +3816,13 @@ BEGIN; AND is_primary_key = 0) 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 2 AS check_id, + SELECT 2 AS check_id, ip.index_sanity_id, - 60 AS Priority, - 'Multiple Index Personalities' AS findings_group, - 'Borderline duplicate keys' AS finding, + 30 AS Priority, + 'Redundant Indexes' AS findings_group, + 'Approximate Duplicate Keys' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/duplicateindex' AS URL, + N'https://www.brentozar.com/go/duplicateindex' AS URL, ip.db_schema_object_indexid AS details, ip.index_definition, ip.secret_columns, @@ -2129,57 +3839,148 @@ BEGIN; di.key_column_names <> ip.key_column_names AND di.number_dupes > 1 ) - AND ip.is_primary_key = 0 - /* WHERE clause skips near-duplicate indexes when getting all databases or using PainRelief mode */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - - ORDER BY ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names + AND ip.is_primary_key = 0 + ORDER BY ips.total_rows DESC, ip.[schema_name], ip.[object_name], ip.key_column_names, ip.include_column_names OPTION ( RECOMPILE ); - END; ---------------------------------------- - --Aggressive Indexes: Check_id 10-19 + --Resumable Indexing: Check_id 122-123 ---------------------------------------- - BEGIN; + /* + 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 11: Total lock wait time > 5 minutes (row + page) with long average waits', 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 11 AS check_id, + 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'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with long average waits' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) - AS NVARCHAR(30)) AS details, - i.index_definition, + 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, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) > 5000 - GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id + '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 ); - RAISERROR(N'check_id 12: Total lock wait time > 5 minutes (row + page) with short average waits', 0,1) WITH NOWAIT; + ---------------------------------------- + --Aggressive Indexes: Check_id 10-19 + ---------------------------------------- + + RAISERROR(N'check_id 11: Total lock wait time > 5 minutes (row + page)', 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 12 AS check_id, + SELECT 11 AS check_id, i.index_sanity_id, - 10 AS Priority, - N'Aggressive Indexes' AS findings_group, - N'Total lock wait time > 5 minutes (row + page) with short average waits' AS finding, + 70 AS Priority, + N'Locking-Prone ' + + CASE COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) + WHEN 0 THEN N'Under-Indexing' + WHEN 1 THEN N'Under-Indexing' + WHEN 2 THEN N'Under-Indexing' + WHEN 3 THEN N'Under-Indexing' + WHEN 4 THEN N'Indexes' + WHEN 5 THEN N'Indexes' + WHEN 6 THEN N'Indexes' + WHEN 7 THEN N'Indexes' + WHEN 8 THEN N'Indexes' + WHEN 9 THEN N'Indexes' + ELSE N'Over-Indexing' + END AS findings_group, + N'Total lock wait time > 5 minutes (row + page)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AggressiveIndexes' AS URL, - i.db_schema_object_indexid + N': ' + - sz.index_lock_wait_summary + N' NC indexes on table: ' + - CAST(COALESCE((SELECT SUM(1) FROM #IndexSanity iMe INNER JOIN #IndexSanity iOthers ON iMe.database_id = iOthers.database_id AND iMe.object_id = iOthers.object_id AND iOthers.index_id > 1 WHERE i.index_sanity_id = iMe.index_sanity_id),0) + N'https://www.brentozar.com/go/AggressiveIndexes' AS URL, + (i.db_schema_object_indexid + N': ' + + sz.index_lock_wait_summary + N' NC indexes on table: ') COLLATE DATABASE_DEFAULT + + CAST(COALESCE((SELECT SUM(1) + FROM #IndexSanity iMe + INNER JOIN #IndexSanity iOthers + ON iMe.database_id = iOthers.database_id + AND iMe.object_id = iOthers.object_id + AND iOthers.index_id > 1 + WHERE i.index_sanity_id = iMe.index_sanity_id + AND iOthers.is_hypothetical = 0 + AND iOthers.is_disabled = 0 + ), 0) AS NVARCHAR(30)) AS details, i.index_definition, i.secret_columns, @@ -2188,26 +3989,25 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id WHERE (total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) > 300000 - AND (sz.avg_page_lock_wait_in_ms + sz.avg_row_lock_wait_in_ms) < 5000 GROUP BY i.index_sanity_id, [database_name], i.db_schema_object_indexid, sz.index_lock_wait_summary, i.index_definition, i.secret_columns, i.index_usage_summary, sz.index_size_summary, sz.index_sanity_id + ORDER BY SUM(total_row_lock_wait_in_ms + total_page_lock_wait_in_ms) DESC, 4, [database_name], 8 OPTION ( RECOMPILE ); - END; + ---------------------------------------- --Index Hoarder: Check_id 20-29 ---------------------------------------- - BEGIN - RAISERROR(N'check_id 20: >=7 NC indexes on any given table. Yes, 7 is an arbitrary number.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 20: >= 10 NC indexes on any given table. Yes, 10 is an arbitrary number.', 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 20 AS check_id, + SELECT 20 AS check_id, MAX(i.index_sanity_id) AS index_sanity_id, - 100 AS Priority, - 'Index Hoarder' AS findings_group, - 'Many NC indexes on a single table' AS finding, + 10 AS Priority, + 'Over-Indexing' AS findings_group, + 'Many NC Indexes on a Single Table' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, CAST (COUNT(*) AS NVARCHAR(30)) + ' NC indexes on ' + i.db_schema_object_name AS details, i.db_schema_object_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + ' indexes)' AS index_definition, '' AS secret_columns, @@ -2223,83 +4023,27 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id WHERE index_id NOT IN ( 0, 1 ) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) GROUP BY db_schema_object_name, [i].[database_name] - HAVING COUNT(*) >= 7 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - IF @Filter = 1 /*@Filter=1 is "ignore unusued" */ - BEGIN - RAISERROR(N'Skipping checks on unused indexes (21 and 22) because @Filter=1', 0,1) WITH NOWAIT; - END; - ELSE /*Otherwise, go ahead and do the checks*/ - BEGIN - RAISERROR(N'check_id 21: >=5 percent of indexes are unused. Yes, 5 is an arbitrary number.', 0,1) WITH NOWAIT; - DECLARE @percent_NC_indexes_unused NUMERIC(29,1); - DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); - - SELECT @percent_NC_indexes_unused =( 100.00 * SUM(CASE WHEN total_reads = 0 THEN 1 - ELSE 0 - END) ) / COUNT(*) , - @NC_indexes_unused_reserved_MB = SUM(CASE WHEN total_reads = 0 THEN sz.total_reserved_MB - ELSE 0 - END) - FROM #IndexSanity i - 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()) - 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, - secret_columns, index_usage_summary, index_size_summary ) - SELECT 21 AS check_id, - MAX(i.index_sanity_id) AS index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'More than 5 percent NC indexes are unused' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + - N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, - i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, - '' AS secret_columns, - CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' - + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, - - REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' - + CASE WHEN SUM(total_reserved_MB) > 1024 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' - WHEN SUM(total_reserved_MB) > 0 THEN - N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' - ELSE '' - END AS index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE index_id NOT IN ( 0, 1 ) - AND i.is_unique = 0 - AND total_reads = 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 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()) - GROUP BY i.database_name - OPTION ( RECOMPILE ); + HAVING COUNT(*) >= 10 + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 22: NC indexes with 0 reads. (Borderline) and >= 10,000 writes', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 22: NC indexes with 0 reads and >= 10,000 writes', 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 22 AS check_id, + SELECT 22 AS check_id, i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with High Writes' AS finding, + 10 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC Index with High Writes' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Reads: 0,' + + N' Writes: ' + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + + N' on: ' + + i.db_schema_object_indexid + AS details, i.index_definition, i.secret_columns, i.index_usage_summary, @@ -2311,388 +4055,87 @@ 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 + AND @Filter <> 1 /* 1 = "ignore unused */ ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); - END; /*end checks only run when @Filter <> 1*/ - RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Borderline: Wide indexes (7 or more columns)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' - + i.db_schema_object_indexid AS details, i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexSanity AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE ( count_key_columns + count_included_columns ) >= 7 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT database_id, [object_id], - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND key_ordinal > 0 - GROUP BY database_id, object_id - ) - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + RAISERROR(N'check_id 34: Filtered index definition columns not in index definition', 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 24 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide clustered index (> 3 columns OR > 16 bytes)' AS finding, + SELECT 34 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Filter Columns Not In Index Definition' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' - + CAST(cc.sum_max_length AS NVARCHAR(10)) - + N' bytes in clustered index:' + i.db_schema_object_name - + N'. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'The index ' + + QUOTENAME(i.index_name) + + N' on [' + + i.db_schema_object_name + + N'] has a filter on [' + + i.filter_definition + + N'] but is missing [' + + LTRIM(i.filter_columns_not_in_index) + + N'] from the index definition.' + AS details, + i.index_definition, + i.secret_columns, i.index_usage_summary, - ip.index_size_summary + sz.index_size_summary FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND i.database_id = cc.database_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (count_key_columns > 3 /*More than three key columns.*/ - OR cc.sum_max_length > 16 /*More than 16 bytes in key */) - AND i.is_CX_columnstore = 0 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 25: Addicted to nullable columns.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) AS non_nullable_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 25 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to nulls' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = ip.database_id - AND cc.[schema_name] = ip.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND cc.non_nullable_columns < 2 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE max_length WHEN -1 THEN 1 ELSE 0 END) AS count_lob_columns, - SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 26 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Wide tables: 35+ cols or > 2000 non-LOB bytes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' has ' + CAST((total_columns) AS NVARCHAR(10)) - + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) - + N' bytes.' + - CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) AS NVARCHAR(10)) - + ' columns are LOB types.' ELSE '' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND - (cc.total_columns >= 35 OR - cc.sum_max_length >= 2000) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 27: Addicted to strings.', 0,1) WITH NOWAIT; - WITH count_columns AS ( - SELECT [object_id], - [database_id], - [schema_name], - SUM(CASE WHEN system_type_name IN ('varchar','nvarchar','char') OR max_length=-1 THEN 1 ELSE 0 END) AS string_or_LOB_columns, - COUNT(*) AS total_columns - FROM #IndexColumns ic - WHERE index_id IN (1,0) /*Heap or clustered only*/ - GROUP BY [object_id], - [database_id], - [schema_name] - ) - 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 27 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Index Hoarder' AS findings_group, - N'Addicted to strings' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - i.db_schema_object_name - + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) - + N' of ' + CAST(total_columns AS NVARCHAR(10)) - + N' columns. Check if data types are valid.' AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] - AND cc.database_id = i.database_id - AND cc.[schema_name] = i.[schema_name] - CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 - WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND calc1.non_string_or_lob_columns <= 1 - AND cc.total_columns > 3 - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, - i.index_sanity_id, - 100 AS Priority, - N'Index Hoarder' AS findings_group, - N'Non-Unique clustered index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name - + N' and all NC indexes. ' + - (SELECT CAST(COUNT(*) AS NVARCHAR(23)) FROM #IndexSanity i2 - WHERE i2.[object_id]=i.[object_id] AND i2.database_id = i.database_id AND i2.index_id <> 1 - AND i2.is_disabled=0 AND i2.is_hypothetical=0) - + N' NC indexes on the table.' - AS details, - i.index_definition, - secret_columns, - i.index_usage_summary, - ip.index_size_summary - FROM #IndexSanity i - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE index_id = 1 /* clustered only */ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - AND is_unique=0 /* not unique */ - AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.filter_columns_not_in_index IS NOT NULL + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 29: NC indexes with 0 reads. (Borderline) and < 10,000 writes', 0,1) WITH NOWAIT; - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + 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 29 AS check_id, + SELECT 124 AS check_id, i.index_sanity_id, - 150 AS Priority, - N'Index Hoarder' AS findings_group, - N'Unused NC index with Low Writes' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, - N'0 reads: ' + i.db_schema_object_indexid AS details, + 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 AS i - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.total_reads=0 - AND i.user_updates < 10000 - 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 + 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 ); - END; - ---------------------------------------- - --Feature-Phobic Indexes: Check_id 30-39 - ---------------------------------------- - BEGIN - RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; - /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 - */ - - SELECT database_name, - SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, - 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes - INTO #index_includes - FROM #IndexSanity - GROUP BY database_name; - - IF NOT (@Mode = 0) - 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 30 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - database_name AS [Database Name], - N'No indexes use includes' AS finding, 'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'No indexes use includes' AS details, - database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes = 0 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 0,1) WITH NOWAIT; - IF NOT (@Mode = 0) - 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 31 AS check_id, - NULL AS index_sanity_id, - 150 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: Includes are used in < 3% of indexes' AS findings, - database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, - N'Entire database' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #index_includes - WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 32: filtered indexes and indexed views', 0,1) WITH NOWAIT; - - IF NOT (@Mode = 0) - 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 DISTINCT - 32 AS check_id, - NULL AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Borderline: No filtered indexes or indexed views exist' AS finding, - i.database_name AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'These are NOT always needed-- but do you know when you would use them?' AS details, - i.database_name + N' (Entire database)' AS index_definition, - N'' AS secret_columns, - N'N/A' AS index_usage_summary, - N'N/A' AS index_size_summary - FROM #IndexSanity i - WHERE i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE filter_definition <> '' ) - AND i.database_name NOT IN ( - SELECT database_name - FROM #IndexSanity - WHERE is_indexed_view = 1 ) - OPTION ( RECOMPILE ); - END; - - RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, - i.index_sanity_id AS index_sanity_id, - 250 AS Priority, - N'Feature-Phobic Indexes' AS findings_group, - N'Potential filtered index (based on column name)' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexFeatures' AS URL, - N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - sz.index_size_summary - FROM #IndexColumns ic - JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] - AND ic.database_id =i.database_id - AND ic.schema_name = i.schema_name - AND ic.[index_id]=i.[index_id] - AND i.[index_id] > 1 /* non-clustered index */ - JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - WHERE (column_name LIKE 'is%' - OR column_name LIKE '%archive%' - OR column_name LIKE '%active%' - OR column_name LIKE '%flag%') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - ---------------------------------------- --Self Loathing Indexes : Check_id 40-49 ---------------------------------------- - BEGIN RAISERROR(N'check_id 40: Fillfactor in nonclustered 80 percent or less', 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 40 AS check_id, + SELECT 40 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: nonclustered index' AS finding, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Nonclustered Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, CAST(fill_factor AS NVARCHAR(10)) + N'% fill factor on ' + db_schema_object_indexid + N'. '+ CASE WHEN (last_user_update IS NULL OR user_updates < 1) THEN N'No writes have been made.' @@ -2708,19 +4151,18 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id > 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); RAISERROR(N'check_id 40: Fillfactor in clustered 80 percent or less', 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 40 AS check_id, + SELECT 40 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Low Fill Factor: clustered index' AS finding, + N'Indexes Worth Reviewing' AS findings_group, + N'Low Fill Factor on Clustered Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Fill factor on ' + db_schema_object_indexid + N' is ' + CAST(fill_factor AS NVARCHAR(10)) + N'%. '+ CASE WHEN (last_user_update IS NULL OR user_updates < 1) THEN N'No writes have been made.' @@ -2736,53 +4178,10 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id WHERE index_id = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND fill_factor BETWEEN 1 AND 80 OPTION ( RECOMPILE ); - RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, - i.index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Hypothetical Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Hypothetical Index: ' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - N'' AS index_usage_summary, - N'' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_hypothetical = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - - RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; - --Note: disabled NC indexes will have O rows in #IndexSanitySize! - 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 42 AS check_id, - index_sanity_id, - 150 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Disabled Index' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - N'Disabled Index:' + db_schema_object_indexid AS details, - i.index_definition, - i.secret_columns, - i.index_usage_summary, - 'DISABLED' AS index_size_summary - FROM #IndexSanity AS i - WHERE is_disabled = 1 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 43: Heaps with forwarded records or deletes', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 43: Heaps with forwarded records', 0,1) WITH NOWAIT; WITH heaps_cte AS ( SELECT [object_id], [database_id], @@ -2793,19 +4192,22 @@ BEGIN; GROUP BY [object_id], [database_id], [schema_name] - HAVING SUM(forwarded_fetch_count) > 0 - OR SUM(leaf_delete_count) > 0) + HAVING SUM(forwarded_fetch_count) > 0) 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 43 AS check_id, + SELECT 43 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Heaps with forwarded records or deletes' AS finding, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Forwarded Fetches' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, - CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + ' forwarded fetches, ' - + CAST(h.leaf_delete_count AS NVARCHAR(256)) + ' deletes against heap:' + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CASE WHEN h.forwarded_fetch_count >= 922337203685477 THEN '>= 922,337,203,685,477' + WHEN @DaysUptime < 1 THEN CAST(h.forwarded_fetch_count AS NVARCHAR(256)) + N' forwarded fetches against heap: ' + db_schema_object_indexid + ELSE REPLACE(CONVERT(NVARCHAR(256),CAST(CAST( + (h.forwarded_fetch_count /*/@DaysUptime */) + AS BIGINT) AS MONEY), 1), '.00', '') + END + N' forwarded fetches per day against heap: ' + db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -2817,6 +4219,7 @@ BEGIN; AND i.[schema_name] = h.[schema_name] JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.index_id = 0 + AND h.forwarded_fetch_count / @DaysUptime > 1000 AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END OPTION ( RECOMPILE ); @@ -2835,13 +4238,13 @@ BEGIN; OR SUM(leaf_delete_count) > 0) 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 44 AS check_id, + SELECT 44 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, - N'Large Active heap' AS finding, + N'Indexes Worth Reviewing' AS findings_group, + N'Large Active Heap' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Should this table be a heap? ' + db_schema_object_indexid AS details, i.index_definition, 'N/A' AS secret_columns, @@ -2853,11 +4256,9 @@ BEGIN; AND i.[schema_name] = h.[schema_name] JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.index_id = 0 - AND - (i.total_reads > 0 OR i.user_updates > 0) + AND (i.total_reads > 0 OR i.user_updates > 0) AND sz.total_rows >= 100000 AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); RAISERROR(N'check_id 45: Medium Heaps with reads or writes.', 0,1) WITH NOWAIT; @@ -2875,13 +4276,13 @@ BEGIN; OR SUM(leaf_delete_count) > 0) 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 45 AS check_id, + SELECT 45 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, + N'Indexes Worth Reviewing' AS findings_group, N'Medium Active heap' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Should this table be a heap? ' + db_schema_object_indexid AS details, i.index_definition, 'N/A' AS secret_columns, @@ -2897,7 +4298,6 @@ BEGIN; (i.total_reads > 0 OR i.user_updates > 0) AND sz.total_rows >= 10000 AND sz.total_rows < 100000 AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); RAISERROR(N'check_id 46: Small Heaps with reads or writes.', 0,1) WITH NOWAIT; @@ -2915,13 +4315,13 @@ BEGIN; OR SUM(leaf_delete_count) > 0) 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 46 AS check_id, + SELECT 46 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, + N'Indexes Worth Reviewing' AS findings_group, N'Small Active heap' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, N'Should this table be a heap? ' + db_schema_object_indexid AS details, i.index_definition, 'N/A' AS secret_columns, @@ -2937,8 +4337,7 @@ BEGIN; (i.total_reads > 0 OR i.user_updates > 0) AND sz.total_rows < 10000 AND h.[object_id] IS NULL /*don't duplicate the prior check.*/ - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); + OPTION ( RECOMPILE ); RAISERROR(N'check_id 47: Heap with a Nonclustered Primary Key', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -2946,10 +4345,10 @@ BEGIN; SELECT 47 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Self Loathing Indexes' AS findings_group, + N'Indexes Worth Reviewing' AS findings_group, N'Heap with a Nonclustered Primary Key' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/SelfLoathing' AS URL, + N'https://www.brentozar.com/go/SelfLoathing' AS URL, db_schema_object_indexid + N' is a HEAP with a Nonclustered Primary Key' AS details, i.index_definition, i.secret_columns, @@ -2957,23 +4356,31 @@ BEGIN; sz.index_size_summary FROM #IndexSanity i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type = 2 AND i.is_primary_key = 1 AND i.secret_columns LIKE '%RID%' - OPTION ( RECOMPILE ); + WHERE i.index_type = 2 AND i.is_primary_key = 1 + AND EXISTS + ( + SELECT 1/0 + FROM #IndexSanity AS isa + WHERE i.database_id = isa.database_id + AND i.object_id = isa.object_id + AND isa.index_id = 0 + ) + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 48: Nonclustered indexes with a bad read to write ration', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 48: Nonclustered indexes with a bad read to write ratio', 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 48 AS check_id, i.index_sanity_id, 100 AS Priority, - N'Index Hoarder' AS findings_group, + N'Over-Indexing' AS findings_group, N'NC index with High Writes:Reads' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/IndexHoarder' AS URL, + N'https://www.brentozar.com/go/IndexHoarder' AS URL, N'Reads: ' - + CONVERT(NVARCHAR(10), i.total_reads) + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.total_reads) AS MONEY), 1), N'.00', N'') + N' Writes: ' - + CONVERT(NVARCHAR(10), i.user_updates) + + REPLACE(CONVERT(NVARCHAR(30), CAST((i.user_updates) AS MONEY), 1), N'.00', N'') + N' on: ' + i.db_schema_object_indexid AS details, i.index_definition, @@ -2984,6 +4391,7 @@ BEGIN; JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.total_reads > 0 /*Not totally unused*/ AND i.user_updates >= 10000 /*Decent write activity*/ + AND i.total_reads < 10000 AND ((i.total_reads * 10) < i.user_updates) /*10x more writes than reads*/ AND i.index_id NOT IN (0,1) /*NCs only*/ AND i.is_unique = 0 @@ -2991,14 +4399,11 @@ BEGIN; ORDER BY i.db_schema_object_indexid OPTION ( RECOMPILE ); - - END; ---------------------------------------- --Indexaphobia --Missing indexes with value >= 5 million: : Check_id 50-59 ---------------------------------------- - BEGIN - RAISERROR(N'check_id 50: Indexaphobia.', 0,1) WITH NOWAIT; + RAISERROR(N'check_id 50: High Value Missing Index.', 0,1) WITH NOWAIT; WITH index_size_cte AS ( SELECT i.database_id, i.schema_name, @@ -3026,24 +4431,20 @@ BEGIN; AND i.is_disabled = 0 GROUP BY i.database_id, i.schema_name, i.[object_id]) INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, - index_usage_summary, index_size_summary, create_tsql, more_info ) + index_usage_summary, index_size_summary, create_tsql, more_info, sample_query_plan ) SELECT check_id, t.index_sanity_id, t.check_id, t.findings_group, t.finding, t.[Database Name], t.URL, t.details, t.[definition], - index_estimated_impact, t.index_size_summary, create_tsql, more_info + index_estimated_impact, t.index_size_summary, create_tsql, more_info, sample_query_plan FROM ( - SELECT ROW_NUMBER() OVER (ORDER BY mi.is_low, magic_benefit_number DESC) AS rownum, + SELECT ROW_NUMBER() OVER (ORDER BY magic_benefit_number DESC) AS rownum, 50 AS check_id, sz.index_sanity_id, - 10 AS Priority, - N'Indexaphobia' AS findings_group, - N'High value missing index' + CASE mi.is_low - WHEN 0 THEN N' with High Impact' - WHEN 1 THEN N' with Low Impact' - END - AS finding, + 40 AS Priority, + N'Index Suggestion' AS findings_group, + N'High Value Missing Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Indexaphobia' AS URL, + N'https://www.brentozar.com/go/Indexaphobia' AS URL, mi.[statement] + N' Est. benefit per day: ' + CASE WHEN magic_benefit_number >= 922337203685477 THEN '>= 922,337,203,685,477' @@ -3057,34 +4458,782 @@ BEGIN; mi.create_tsql, mi.more_info, magic_benefit_number, - mi.is_low + mi.is_low, + mi.sample_query_plan FROM #MissingIndexes mi LEFT JOIN index_size_cte sz ON mi.[object_id] = sz.object_id AND mi.database_id = sz.database_id AND mi.schema_name = sz.schema_name /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) + WHERE @ShowAllMissingIndexRequests=1 + OR ( @Mode = 4 AND (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) OR (magic_benefit_number / CASE WHEN sz.create_days < @DaysUptime THEN sz.create_days ELSE @DaysUptime END) >= 100000 ) AS t WHERE t.rownum <= CASE WHEN (@Mode <> 4) THEN 20 ELSE t.rownum END - ORDER BY t.is_low, magic_benefit_number DESC; + ORDER BY magic_benefit_number DESC + OPTION ( RECOMPILE ); + + + + RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; + -- Allowed Ranges: + --int -2,147,483,648 to 2,147,483,647 + --smallint -32,768 to 32,768 + --tinyint 0 to 255 + + 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 68 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Within ' + + CAST (calc1.percent_remaining AS NVARCHAR(256)) + + N' Percent End of Range' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + CROSS APPLY ( + SELECT CAST(CASE WHEN ic.increment_value >= 0 + THEN + CASE ic.system_type_name + WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE 999 + END + ELSE --ic.increment_value is negative + CASE ic.system_type_name + WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 + WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 + WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 + ELSE -1 + END + END AS NUMERIC(5,1)) AS percent_remaining + ) AS calc1 + WHERE i.index_id IN (1,0) + AND calc1.percent_remaining <= 30 + OPTION (RECOMPILE); + + + RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; + IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) + AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) + BEGIN + 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 72 AS check_id, + i.index_sanity_id, + 80 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + 'Columnstore Indexes with Trace Flag 834' AS finding, + [database_name] AS [Database Name], + N'https://support.microsoft.com/en-us/kb/3210239' AS URL, + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + ISNULL(sz.index_size_summary,'') AS index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_type IN (5,6) + OPTION ( RECOMPILE ); + END; + + ---------------------------------------- + --Statistics Info: Check_id 90-99, as well as 125 + ---------------------------------------- + + RAISERROR(N'check_id 90: Outdated 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 ) + SELECT 90 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics Not Updated Recently', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Statistics on this table were last updated ' + + CASE WHEN s.last_statistics_update IS NULL THEN N' NEVER ' + ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + + ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + + ' modifications in that time, which is ' + + CONVERT(NVARCHAR(100), s.percent_modifications) + + '% of the table.' + END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 30) + AND s.percent_modifications >= 10. + AND s.rows >= 10000 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Low Sampling Rates', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows BETWEEN 10000 AND 1000000 AND s.percent_sampled < 10) + 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 ) + SELECT 92 AS check_id, + 90 AS Priority, + 'Statistics Warnings' AS findings_group, + 'Statistics With NO RECOMPUTE', + s.database_name, + 'https://www.brentozar.com/go/stats' AS URL, + 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.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 ) + SELECT 94 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Check Constraint with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/computedscalar' AS URL, + 'The check constraint ' + QUOTENAME(cc.constraint_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #CheckConstraints AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 99: Computed Columns 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 ) + SELECT 99 AS check_id, + 100 AS Priority, + 'Forced Serialization' AS findings_group, + 'Computed Column with Scalar UDF' AS finding, + cc.database_name, + 'https://www.brentozar.com/go/serialudf' AS URL, + 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition + + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, + cc.column_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #ComputedColumns AS cc + WHERE cc.is_function = 1 + OPTION ( RECOMPILE ); + + + + END /* IF @Mode IN (0, 4) DIAGNOSE priorities 1-100 */ + + + + + + + + + IF @Mode = 4 /* DIAGNOSE*/ + BEGIN; + RAISERROR(N'@Mode=4, running rules for priorities 101+.', 0,1) WITH NOWAIT; + + RAISERROR(N'check_id 21: More Than 5 Percent NC Indexes Are Unused', 0,1) WITH NOWAIT; + DECLARE @percent_NC_indexes_unused NUMERIC(29,1); + DECLARE @NC_indexes_unused_reserved_MB NUMERIC(29,1); + + SELECT @percent_NC_indexes_unused = ( 100.00 * SUM(CASE + WHEN total_reads = 0 + THEN 1 + ELSE 0 + END) ) / COUNT(*), + @NC_indexes_unused_reserved_MB = SUM(CASE + WHEN total_reads = 0 + THEN sz.total_reserved_MB + ELSE 0 + END) + FROM #IndexSanity i + 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()) + 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, + secret_columns, index_usage_summary, index_size_summary ) + SELECT 21 AS check_id, + MAX(i.index_sanity_id) AS index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'More Than 5 Percent NC Indexes Are Unused' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (@percent_NC_indexes_unused AS NVARCHAR(30)) + N' percent NC indexes (' + CAST(COUNT(*) AS NVARCHAR(10)) + N') unused. ' + + N'These take up ' + CAST (@NC_indexes_unused_reserved_MB AS NVARCHAR(30)) + N'MB of space.' AS details, + i.database_name + ' (' + CAST (COUNT(*) AS NVARCHAR(30)) + N' indexes)' AS index_definition, + '' AS secret_columns, + CAST(SUM(total_reads) AS NVARCHAR(256)) + N' reads (ALL); ' + + CAST(SUM([user_updates]) AS NVARCHAR(256)) + N' writes (ALL)' AS index_usage_summary, + + REPLACE(CONVERT(NVARCHAR(30),CAST(MAX([total_rows]) AS MONEY), 1), '.00', '') + N' rows (MAX)' + + CASE WHEN SUM(total_reserved_MB) > 1024 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB)/1024. AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'GB (ALL)' + WHEN SUM(total_reserved_MB) > 0 THEN + N'; ' + CAST(CAST(SUM(total_reserved_MB) AS NUMERIC(29,1)) AS NVARCHAR(30)) + 'MB (ALL)' + ELSE '' + END AS index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + 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()) + GROUP BY i.database_name + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 23: Indexes with 7 or more columns. (Borderline)', 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 23 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Approximate: Wide Indexes (7 or More Columns)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST(count_key_columns + count_included_columns AS NVARCHAR(10)) + ' columns on ' + + i.db_schema_object_indexid AS details, i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE ( count_key_columns + count_included_columns ) >= 7 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 24: Wide clustered indexes (> 3 columns or > 16 bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT database_id, [object_id], + SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + AND key_ordinal > 0 + GROUP BY database_id, object_id + ) + 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 24 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Wide Clustered Index (> 3 columns OR > 16 bytes)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + CAST (i.count_key_columns AS NVARCHAR(10)) + N' columns with potential size of ' + + CAST(cc.sum_max_length AS NVARCHAR(10)) + + N' bytes in clustered index:' + i.db_schema_object_name + + N'. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND i.database_id = cc.database_id + WHERE index_id = 1 /* clustered only */ + AND + (count_key_columns > 3 /*More than three key columns.*/ + OR cc.sum_max_length > 16 /*More than 16 bytes in key */) + AND i.is_CX_columnstore = 0 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 25: High ratio of nullable columns.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE is_nullable WHEN 1 THEN 0 ELSE 1 END) AS non_nullable_columns, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 25 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Over-Indexing' AS findings_group, + N'High Ratio of Nulls' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' allows null in ' + CAST((total_columns-non_nullable_columns) AS NVARCHAR(10)) + + N' of ' + CAST(total_columns AS NVARCHAR(10)) + + N' columns.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = ip.database_id + AND cc.[schema_name] = ip.[schema_name] + WHERE i.index_id IN (1,0) + AND cc.non_nullable_columns < 2 + AND cc.total_columns > 3 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 26: Wide tables (35+ cols or > 2000 non-LOB bytes).', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE max_length WHEN -1 THEN 1 ELSE 0 END) AS count_lob_columns, + SUM(CASE max_length WHEN -1 THEN 0 ELSE max_length END) AS sum_max_length, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 26 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Wide Tables: 35+ cols or > 2000 non-LOB bytes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' has ' + CAST((total_columns) AS NVARCHAR(10)) + + N' total columns with a max possible width of ' + CAST(sum_max_length AS NVARCHAR(10)) + + N' bytes.' + + CASE WHEN count_lob_columns > 0 THEN CAST((count_lob_columns) AS NVARCHAR(10)) + + ' columns are LOB types.' ELSE '' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = i.database_id + AND cc.[schema_name] = i.[schema_name] + WHERE i.index_id IN (1,0) + AND + (cc.total_columns >= 35 OR + cc.sum_max_length >= 2000) + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 27: High Ratio of Strings.', 0,1) WITH NOWAIT; + WITH count_columns AS ( + SELECT [object_id], + [database_id], + [schema_name], + SUM(CASE WHEN system_type_name IN ('varchar','nvarchar','char') OR max_length=-1 THEN 1 ELSE 0 END) AS string_or_LOB_columns, + COUNT(*) AS total_columns + FROM #IndexColumns ic + WHERE index_id IN (1,0) /*Heap or clustered only*/ + GROUP BY [object_id], + [database_id], + [schema_name] + ) + 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 27 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Over-Indexing' AS findings_group, + N'High Ratio of Strings' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + i.db_schema_object_name + + N' uses string or LOB types for ' + CAST((string_or_LOB_columns) AS NVARCHAR(10)) + + N' of ' + CAST(total_columns AS NVARCHAR(10)) + + N' columns. Check if data types are valid.' AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + JOIN count_columns AS cc ON i.[object_id]=cc.[object_id] + AND cc.database_id = i.database_id + AND cc.[schema_name] = i.[schema_name] + CROSS APPLY (SELECT cc.total_columns - string_or_LOB_columns AS non_string_or_lob_columns) AS calc1 + WHERE i.index_id IN (1,0) + AND calc1.non_string_or_lob_columns <= 1 + AND cc.total_columns > 3 + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 28: Non-unique clustered 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 28 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Non-Unique Clustered Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'Uniquifiers will be required! Clustered index: ' + i.db_schema_object_name + + N' and all NC indexes. ' + + (SELECT CAST(COUNT(*) AS NVARCHAR(23)) + FROM #IndexSanity i2 + WHERE i2.[object_id]=i.[object_id] + AND i2.database_id = i.database_id + AND i2.index_id <> 1 + AND i2.is_disabled=0 + AND i2.is_hypothetical=0) + + N' NC indexes on the table.' + AS details, + i.index_definition, + secret_columns, + i.index_usage_summary, + ip.index_size_summary + FROM #IndexSanity i + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE index_id = 1 /* clustered only */ + AND is_unique=0 /* not unique */ + AND is_CX_columnstore=0 /* not a clustered columnstore-- no unique option on those */ + ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 29: NC indexes with 0 reads and < 10,000 writes', 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 29 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Over-Indexing' AS findings_group, + N'Unused NC index with Low Writes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexHoarder' AS URL, + N'0 reads: ' + i.db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity AS i + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.total_reads=0 + AND i.user_updates < 10000 + 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()) + ORDER BY i.db_schema_object_indexid + OPTION ( RECOMPILE ); + + + ---------------------------------------- + --Feature-Phobic Indexes: Check_id 30-39 + ---------------------------------------- + RAISERROR(N'check_id 30: No indexes with includes', 0,1) WITH NOWAIT; + /* This does not work the way you'd expect with @GetAllDatabases = 1. For details: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/825 + */ + + SELECT database_name, + SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) AS number_indexes_with_includes, + 100.* SUM(CASE WHEN count_included_columns > 0 THEN 1 ELSE 0 END) / ( 1.0 * COUNT(*) ) AS percent_indexes_with_includes + INTO #index_includes + FROM #IndexSanity + WHERE is_hypothetical = 0 + AND is_disabled = 0 + AND NOT (@GetAllDatabases = 1 OR @Mode = 0) + GROUP BY database_name; + + 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 30 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + database_name AS [Database Name], + N'No Indexes Use Includes' AS finding, 'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'No Indexes Use Includes' AS details, + database_name + N' (Entire database)' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #index_includes + WHERE number_indexes_with_includes = 0 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 31: < 3 percent of indexes have includes', 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 31 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Few Indexes Use Includes' AS findings, + database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'Only ' + CAST(percent_indexes_with_includes AS NVARCHAR(20)) + '% of indexes have includes' AS details, + N'Entire database' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #index_includes + WHERE number_indexes_with_includes > 0 AND percent_indexes_with_includes <= 3 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 32: filtered indexes and indexed views', 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 DISTINCT + 32 AS check_id, + NULL AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'No Filtered Indexes or Indexed Views' AS finding, + i.database_name AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'These are NOT always needed-- but do you know when you would use them?' AS details, + i.database_name + N' (Entire database)' AS index_definition, + N'' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary + FROM #IndexSanity i + WHERE i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE filter_definition <> '' ) + AND i.database_name NOT IN ( + SELECT database_name + FROM #IndexSanity + WHERE is_indexed_view = 1 ) + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 33: Potential filtered indexes based on column names.', 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 33 AS check_id, + i.index_sanity_id AS index_sanity_id, + 250 AS Priority, + N'Omitted Index Features' AS findings_group, + N'Potential Filtered Index (Based on Column Name)' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/IndexFeatures' AS URL, + N'A column name in this index suggests it might be a candidate for filtering (is%, %archive%, %active%, %flag%)' AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexColumns ic + JOIN #IndexSanity i ON ic.[object_id]=i.[object_id] + AND ic.database_id =i.database_id + AND ic.schema_name = i.schema_name + AND ic.[index_id]=i.[index_id] + AND i.[index_id] > 1 /* non-clustered index */ + JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + WHERE (column_name LIKE 'is%' + OR column_name LIKE '%archive%' + OR column_name LIKE '%active%' + OR column_name LIKE '%flag%') + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 41: Hypothetical indexes ', 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 41 AS check_id, + i.index_sanity_id, + 150 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Hypothetical Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Hypothetical Index: ' + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + N'' AS index_usage_summary, + N'' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_hypothetical = 1 + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 42: Disabled indexes', 0,1) WITH NOWAIT; + --Note: disabled NC indexes will have O rows in #IndexSanitySize! + 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 42 AS check_id, + index_sanity_id, + 150 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Disabled Index' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + N'Disabled Index:' + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + 'DISABLED' AS index_size_summary + FROM #IndexSanity AS i + WHERE is_disabled = 1 + OPTION ( RECOMPILE ); + RAISERROR(N'check_id 49: Heaps with deletes', 0,1) WITH NOWAIT; + WITH heaps_cte + AS ( SELECT [object_id], + [database_id], + [schema_name], + SUM(leaf_delete_count) AS leaf_delete_count + FROM #IndexPartitionSanity + GROUP BY [object_id], + [database_id], + [schema_name] + HAVING SUM(forwarded_fetch_count) < 1000 * @DaysUptime /* Only alert about indexes with no forwarded fetches - we already alerted about those in check_id 43 */ + AND SUM(leaf_delete_count) > 0) + 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 49 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Indexes Worth Reviewing' AS findings_group, + N'Heaps with Deletes' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/SelfLoathing' AS URL, + CAST(h.leaf_delete_count AS NVARCHAR(256)) + N' deletes against heap:' + + db_schema_object_indexid AS details, + i.index_definition, + i.secret_columns, + i.index_usage_summary, + sz.index_size_summary + FROM #IndexSanity i + JOIN heaps_cte h ON i.[object_id] = h.[object_id] + AND i.[database_id] = h.[database_id] + AND i.[schema_name] = h.[schema_name] + JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id + WHERE i.index_id = 0 + AND sz.total_reserved_MB >= CASE WHEN NOT (@GetAllDatabases = 1 OR @Mode = 4) THEN @ThresholdMB ELSE sz.total_reserved_MB END + OPTION ( RECOMPILE ); - END; ---------------------------------------- --Abnormal Psychology : Check_id 60-79 ---------------------------------------- - BEGIN RAISERROR(N'check_id 60: XML indexes', 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 60 AS check_id, + SELECT 60 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'XML Indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'XML Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3092,21 +5241,22 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_XML = 1 OPTION ( RECOMPILE ); + WHERE i.is_XML = 1 + OPTION ( RECOMPILE ); RAISERROR(N'check_id 61: Columnstore indexes', 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 61 AS check_id, + SELECT 61 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, + N'Abnormal Design Pattern' AS findings_group, CASE WHEN i.is_NC_columnstore=1 THEN N'NC Columnstore Index' ELSE N'Clustered Columnstore Index' END AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3121,13 +5271,13 @@ BEGIN; RAISERROR(N'check_id 62: Spatial indexes', 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 62 AS check_id, + SELECT 62 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Spatial indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Spatial Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3135,18 +5285,19 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.is_spatial = 1 OPTION ( RECOMPILE ); + WHERE i.is_spatial = 1 + OPTION ( RECOMPILE ); RAISERROR(N'check_id 63: Compressed indexes', 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 63 AS check_id, + SELECT 63 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Compressed indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Compressed Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid + N'. COMPRESSION: ' + sz.data_compression_desc AS details, i.index_definition, i.secret_columns, @@ -3154,18 +5305,19 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' OPTION ( RECOMPILE ); + WHERE sz.data_compression_desc LIKE '%PAGE%' OR sz.data_compression_desc LIKE '%ROW%' + OPTION ( RECOMPILE ); RAISERROR(N'check_id 64: Partitioned', 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 64 AS check_id, + SELECT 64 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Partitioned indexes' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Partitioned Index' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3173,18 +5325,19 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.partition_key_column_name IS NOT NULL OPTION ( RECOMPILE ); + WHERE i.partition_key_column_name IS NOT NULL + OPTION ( RECOMPILE ); RAISERROR(N'check_id 65: Non-Aligned Partitioned', 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 65 AS check_id, + SELECT 65 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Non-Aligned index on a partitioned table' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Non-Aligned Index on a Partitioned Table' AS finding, i.[database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3199,18 +5352,18 @@ BEGIN; AND iParent.partition_key_column_name IS NOT NULL /* parent is partitioned*/ JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.partition_key_column_name IS NULL - OPTION ( RECOMPILE ); + OPTION ( RECOMPILE ); RAISERROR(N'check_id 66: Recently created tables/indexes (1 week)', 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 66 AS check_id, + SELECT 66 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently created tables/indexes (1 week)' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Created Tables/Indexes (1 week)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid + N' was created on ' + CONVERT(NVARCHAR(16),i.create_date,121) + N'. Tables/indexes which are dropped/created regularly require special methods for index tuning.' @@ -3222,19 +5375,18 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.create_date >= DATEADD(dd,-7,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - OPTION ( RECOMPILE ); + OPTION ( RECOMPILE ); RAISERROR(N'check_id 67: Recently modified tables/indexes (2 days)', 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 67 AS check_id, + SELECT 67 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Recently modified tables/indexes (2 days)' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Recently Modified Tables/Indexes (2 days)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid + N' was modified on ' + CONVERT(NVARCHAR(16),i.modify_date,121) + N'. A large amount of recently modified indexes may mean a lot of rebuilds are occurring each night.' @@ -3246,134 +5398,32 @@ BEGIN; FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id WHERE i.modify_date > DATEADD(dd,-2,GETDATE()) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) AND /*Exclude recently created tables.*/ i.create_date < DATEADD(dd,-7,GETDATE()) - OPTION ( RECOMPILE ); - - RAISERROR(N'check_id 68: Identity columns within 30 percent of the end of range', 0,1) WITH NOWAIT; - -- Allowed Ranges: - --int -2,147,483,648 to 2,147,483,647 - --smallint -32,768 to 32,768 - --tinyint 0 to 255 - - 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 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column within ' + - CAST (calc1.percent_remaining AS NVARCHAR(256)) - + N' percent end of range' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - CROSS APPLY ( - SELECT CAST(CASE WHEN ic.increment_value >= 0 - THEN - CASE ic.system_type_name - WHEN 'int' THEN (2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN (32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ( 255 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE 999 - END - ELSE --ic.increment_value is negative - CASE ic.system_type_name - WHEN 'int' THEN ABS(-2147483647 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 2147483647.*100 - WHEN 'smallint' THEN ABS(-32768 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 32768.*100 - WHEN 'tinyint' THEN ABS( 0 - (ISNULL(ic.last_value,ic.seed_value) + ic.increment_value)) / 255.*100 - ELSE -1 - END - END AS NUMERIC(5,1)) AS percent_remaining - ) AS calc1 - WHERE i.index_id IN (1,0) - AND calc1.percent_remaining <= 30 - UNION ALL - SELECT 68 AS check_id, - i.index_sanity_id, - 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Identity column using a negative seed or increment other than 1' AS finding, - [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) - + N' is an identity with type ' + ic.system_type_name - + N', last value of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.last_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', seed of ' - + ISNULL(REPLACE(CONVERT(NVARCHAR(256),CAST(CAST(ic.seed_value AS BIGINT) AS MONEY), 1), '.00', ''),N'NULL') - + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) - + N', and range of ' + - CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' - WHEN 'smallint' THEN N'+/- 32,768' - WHEN 'tinyint' THEN N'0 to 255' - END - AS details, - i.index_definition, - secret_columns, - ISNULL(i.index_usage_summary,''), - ISNULL(ip.index_size_summary,'') - FROM #IndexSanity i - JOIN #IndexColumns ic ON - i.object_id=ic.object_id - AND i.database_id = ic.database_id - AND i.schema_name = ic.schema_name - AND i.index_id IN (0,1) /* heaps and cx only */ - AND ic.is_identity=1 - AND ic.system_type_name IN ('tinyint', 'smallint', 'int') - JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id - WHERE i.index_id IN (1,0) - AND (ic.seed_value < 0 OR ic.increment_value <> 1) - ORDER BY finding, details DESC OPTION ( RECOMPILE ); + OPTION ( RECOMPILE ); RAISERROR(N'check_id 69: Column collation does not match database collation', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], database_id, schema_name, - COUNT(*) AS column_count + COUNT(*) AS column_count FROM #IndexColumns ic WHERE index_id IN (1,0) /*Heap or clustered only*/ - AND collation_name <> @collation + AND collation_name <> @collation GROUP BY [object_id], database_id, schema_name ) 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 69 AS check_id, + SELECT 69 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Column collation does not match database collation' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Column Collation Does Not Match Database Collation' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_name + N' has ' + CAST(column_count AS NVARCHAR(20)) + N' column' + CASE WHEN column_count > 1 THEN 's' ELSE '' END @@ -3389,16 +5439,15 @@ BEGIN; AND cc.database_id = i.database_id AND cc.schema_name = i.schema_name WHERE i.index_id IN (1,0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); RAISERROR(N'check_id 70: Replicated columns', 0,1) WITH NOWAIT; WITH count_columns AS ( SELECT [object_id], - database_id, - schema_name, - COUNT(*) AS column_count, - SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count + database_id, + schema_name, + COUNT(*) AS column_count, + SUM(CASE is_replicated WHEN 1 THEN 1 ELSE 0 END) AS replicated_column_count FROM #IndexColumns ic WHERE index_id IN (1,0) /*Heap or clustered only*/ GROUP BY object_id, @@ -3407,13 +5456,13 @@ BEGIN; ) 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 70 AS check_id, + SELECT 70 AS check_id, i.index_sanity_id, 200 AS Priority, - N'Abnormal Psychology' AS findings_group, - N'Replicated columns' AS finding, + N'Abnormal Design Pattern' AS findings_group, + N'Replicated Columns' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_name + N' has ' + CAST(replicated_column_count AS NVARCHAR(20)) + N' out of ' + CAST(column_count AS NVARCHAR(20)) @@ -3431,28 +5480,27 @@ BEGIN; AND i.schema_name = cc.schema_name WHERE i.index_id IN (1,0) AND replicated_column_count > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.db_schema_object_name DESC OPTION ( RECOMPILE ); + ORDER BY i.db_schema_object_name DESC + OPTION ( RECOMPILE ); RAISERROR(N'check_id 71: Cascading updates or cascading deletes.', 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, more_info ) - SELECT 71 AS check_id, + SELECT 71 AS check_id, NULL AS index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, + N'Abnormal Design Pattern' AS findings_group, N'Cascading Updates or Deletes' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/AbnormalPsychology' AS URL, - N'Foreign Key ' + foreign_key_name + + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + N' on ' + QUOTENAME(parent_object_name) + N'(' + LTRIM(parent_fk_columns) + N')' + N' referencing ' + QUOTENAME(referenced_object_name) + N'(' + LTRIM(referenced_fk_columns) + N')' + N' has settings:' + CASE [delete_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON DELETE ' +[delete_referential_action_desc] END + CASE [update_referential_action_desc] WHEN N'NO_ACTION' THEN N'' ELSE N' ON UPDATE ' + [update_referential_action_desc] END AS details, - [fk].[database_name] - AS index_definition, + [fk].[database_name] AS index_definition, N'N/A' AS secret_columns, N'N/A' AS index_usage_summary, N'N/A' AS index_size_summary, @@ -3461,21 +5509,42 @@ BEGIN; FROM #ForeignKeys fk WHERE ([delete_referential_action_desc] <> N'NO_ACTION' OR [update_referential_action_desc] <> N'NO_ACTION') - AND NOT (@GetAllDatabases = 1 OR @Mode = 0); + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 72: Columnstore indexes with Trace Flag 834', 0,1) WITH NOWAIT; - IF EXISTS (SELECT * FROM #IndexSanity WHERE index_type IN (5,6)) - AND EXISTS (SELECT * FROM #TraceStatus WHERE TraceFlag = 834 AND status = 1) - BEGIN - INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + RAISERROR(N'check_id 72: Unindexed foreign keys.', 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, more_info ) + SELECT 72 AS check_id, + NULL AS index_sanity_id, + 150 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Unindexed Foreign Keys' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + N'Foreign Key ' + QUOTENAME(foreign_key_name) + + N' on ' + QUOTENAME(parent_object_name) + N'' + + N' referencing ' + QUOTENAME(referenced_object_name) + N'' + + N' does not appear to have a supporting index.' AS details, + N'N/A' AS index_definition, + N'N/A' AS secret_columns, + N'N/A' AS index_usage_summary, + N'N/A' AS index_size_summary, + (SELECT TOP 1 more_info FROM #IndexSanity i WHERE i.object_id=fk.parent_object_id AND i.database_id = fk.database_id AND i.schema_name = fk.schema_name) + AS more_info + FROM #UnindexedForeignKeys AS fk + OPTION ( RECOMPILE ); + + + RAISERROR(N'check_id 73: In-Memory OLTP', 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 72 AS check_id, + SELECT 73 AS check_id, i.index_sanity_id, 150 AS Priority, - N'Abnormal Psychology' AS findings_group, - 'Columnstore Indexes are being used in conjunction with trace flag 834. Visit the link to see why this can be a bad idea' AS finding, - [database_name] AS [Database Name], - N'https://support.microsoft.com/en-us/kb/3210239' AS URL, + N'Abnormal Design Pattern' AS findings_group, + N'In-Memory OLTP' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, i.db_schema_object_indexid AS details, i.index_definition, i.secret_columns, @@ -3483,16 +5552,54 @@ BEGIN; ISNULL(sz.index_size_summary,'') AS index_size_summary FROM #IndexSanity AS i JOIN #IndexSanitySize sz ON i.index_sanity_id = sz.index_sanity_id - WHERE i.index_type IN (5,6) - OPTION ( RECOMPILE ); - END; - - END; + WHERE i.is_in_memory_oltp = 1 + OPTION ( RECOMPILE ); + + RAISERROR(N'check_id 74: Identity column with unusual seed', 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 74 AS check_id, + i.index_sanity_id, + 200 AS Priority, + N'Abnormal Design Pattern' AS findings_group, + N'Identity Column Using a Negative Seed or Increment Other Than 1' AS finding, + [database_name] AS [Database Name], + N'https://www.brentozar.com/go/AbnormalPsychology' AS URL, + i.db_schema_object_name + N'.' + QUOTENAME(ic.column_name) + + N' is an identity with type ' + ic.system_type_name + + N', last value of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.last_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', seed of ' + + ISNULL((CONVERT(NVARCHAR(256),CAST(ic.seed_value AS DECIMAL(38,0)), 1)),N'NULL') + + N', increment of ' + CAST(ic.increment_value AS NVARCHAR(256)) + + N', and range of ' + + CASE ic.system_type_name WHEN 'int' THEN N'+/- 2,147,483,647' + WHEN 'smallint' THEN N'+/- 32,768' + WHEN 'tinyint' THEN N'0 to 255' + ELSE 'unknown' + END + AS details, + i.index_definition, + secret_columns, + ISNULL(i.index_usage_summary,''), + ISNULL(ip.index_size_summary,'') + FROM #IndexSanity i + JOIN #IndexColumns ic ON + i.object_id=ic.object_id + AND i.database_id = ic.database_id + AND i.schema_name = ic.schema_name + AND i.index_id IN (0,1) /* heaps and cx only */ + AND ic.is_identity=1 + AND ic.system_type_name IN ('tinyint', 'smallint', 'int') + JOIN #IndexSanitySize ip ON i.index_sanity_id = ip.index_sanity_id + WHERE i.index_id IN (1,0) + AND (ic.seed_value < 0 OR ic.increment_value <> 1) + ORDER BY finding, details DESC + OPTION ( RECOMPILE ); - ---------------------------------------- + ---------------------------------------- --Workaholics: Check_id 80-89 ---------------------------------------- - BEGIN RAISERROR(N'check_id 80: Most scanned indexes (index_usage_stats)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -3506,10 +5613,10 @@ BEGIN; 80 AS check_id, i.index_sanity_id AS index_sanity_id, 200 AS Priority, - N'Workaholics' AS findings_group, - N'Scan-a-lots (index_usage_stats)' AS finding, + N'High Workloads' AS findings_group, + N'Scan-a-lots (index-usage-stats)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, + N'https://www.brentozar.com/go/Workaholics' AS URL, REPLACE(CONVERT( NVARCHAR(50),CAST(i.user_scans AS MONEY),1),'.00','') + N' scans against ' + i.db_schema_object_indexid + N'. Latest scan: ' + ISNULL(CAST(i.last_user_scan AS NVARCHAR(128)),'?') + N'. ' @@ -3521,8 +5628,8 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id WHERE ISNULL(i.user_scans,0) > 0 - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY i.user_scans * iss.total_reserved_MB DESC; + ORDER BY i.user_scans * iss.total_reserved_MB DESC + OPTION ( RECOMPILE ); RAISERROR(N'check_id 81: Top recent accesses (op stats)', 0,1) WITH NOWAIT; INSERT #BlitzIndexResults ( check_id, index_sanity_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, @@ -3534,10 +5641,10 @@ BEGIN; 81 AS check_id, i.index_sanity_id AS index_sanity_id, 200 AS Priority, - N'Workaholics' AS findings_group, - N'Top recent accesses (index_op_stats)' AS finding, + N'High Workloads' AS findings_group, + N'Top Recent Accesses (index-op-stats)' AS finding, [database_name] AS [Database Name], - N'http://BrentOzar.com/go/Workaholics' AS URL, + N'https://www.brentozar.com/go/Workaholics' AS URL, ISNULL(REPLACE( CONVERT(NVARCHAR(50),CAST((iss.total_range_scan_count + iss.total_singleton_lookup_count) AS MONEY),1), N'.00',N'') @@ -3552,127 +5659,37 @@ BEGIN; FROM #IndexSanity i JOIN #IndexSanitySize iss ON i.index_sanity_id=iss.index_sanity_id WHERE (ISNULL(iss.total_range_scan_count,0) > 0 OR ISNULL(iss.total_singleton_lookup_count,0) > 0) - AND NOT (@GetAllDatabases = 1 OR @Mode = 0) - ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC; - - - END; - - ---------------------------------------- - --Statistics Info: Check_id 90-99 - ---------------------------------------- - BEGIN - - RAISERROR(N'check_id 90: Outdated 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 ) - SELECT 90 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Statistic Abandonment Issues', - s.database_name, - '' AS URL, - 'Statistics on this table were last updated ' + - CASE s.last_statistics_update WHEN NULL THEN N' NEVER ' - ELSE CONVERT(NVARCHAR(20), s.last_statistics_update) + - ' have had ' + CONVERT(NVARCHAR(100), s.modification_counter) + - ' modifications in that time, which is ' + - CONVERT(NVARCHAR(100), s.percent_modifications) + - '% of the table.' - END 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.last_statistics_update <= CONVERT(DATETIME, GETDATE() - 7) - AND s.percent_modifications >= 10. - AND s.rows >= 10000; + ORDER BY ((iss.total_range_scan_count + iss.total_singleton_lookup_count) * iss.total_reserved_MB) DESC + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 91: Statistics with a low 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 91 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Antisocial Samples', - s.database_name, - '' AS URL, - 'Only ' + CONVERT(NVARCHAR(100), s.percent_sampled) + '% of the rows were sampled during the last statistics update. This may lead to poor cardinality estimates.' 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.rows_sampled < 1. - AND s.rows >= 10000; - 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 ) - SELECT 92 AS check_id, - 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Cyberphobic Samples', - s.database_name, - '' AS URL, - 'The statistic ' + QUOTENAME(s.statistics_name) + ' is set to not recompute. This can be helpful if data is really skewed, but harmful if you expect automatic statistics updates.' 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.no_recompute = 1; RAISERROR(N'check_id 93: Statistics with filters', 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 93 AS check_id, 200 AS Priority, - 'Functioning Statistaholics' AS findings_group, - 'Filter Fixation', + 'Statistics Warnings' AS findings_group, + 'Statistics With Filters', s.database_name, - '' AS URL, + 'https://www.brentozar.com/go/stats' AS URL, 'The statistic ' + QUOTENAME(s.statistics_name) + ' is filtered on [' + s.filter_definition + ']. It could be part of a filtered index, or just a filtered statistic. This is purely informational.' 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_filter = 1; - - END; - - ---------------------------------------- - --Computed Column Info: Check_id 99-109 - ---------------------------------------- - BEGIN + WHERE s.has_filter = 1 + OPTION ( RECOMPILE ); - RAISERROR(N'check_id 99: Computed Columns 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 ) - SELECT 99 AS check_id, - 50 AS Priority, - 'Cold Calculators' AS findings_group, - 'Serial Forcer' AS finding, - cc.database_name, - '' AS URL, - 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is based on ' + cc.definition - + '. That indicates it may reference a scalar function, or a CLR function with data access, which can cause all queries and maintenance to run serially.' AS details, - cc.column_definition, - 'N/A' AS secret_columns, - 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #ComputedColumns AS cc - WHERE cc.is_function = 1; RAISERROR(N'check_id 100: Computed Columns that are not Persisted.', 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 100 AS check_id, 200 AS Priority, - 'Cold Calculators' AS findings_group, - 'Definition Defeatists' AS finding, + 'Repeated Calculations' AS findings_group, + 'Computed Columns Not Persisted' AS finding, cc.database_name, '' AS URL, 'The computed column ' + QUOTENAME(cc.column_name) + ' on ' + QUOTENAME(cc.schema_name) + '.' + QUOTENAME(cc.table_name) + ' is not persisted, which means it will be calculated when a query runs.' + @@ -3683,19 +5700,17 @@ BEGIN; 'N/A' AS index_usage_summary, 'N/A' AS index_size_summary FROM #ComputedColumns AS cc - WHERE cc.is_persisted = 0; + WHERE cc.is_persisted = 0 + OPTION ( RECOMPILE ); - ---------------------------------------- - --Temporal Table Info: Check_id 110-119 - ---------------------------------------- RAISERROR(N'check_id 110: Temporal Tables.', 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 110 AS check_id, 200 AS Priority, - 'Temporal Tables' AS findings_group, - 'Obsessive Compulsive Tables', + 'Abnormal Design Pattern' AS findings_group, + 'Temporal Tables', t.database_name, '' AS URL, 'The table ' + QUOTENAME(t.schema_name) + '.' + QUOTENAME(t.table_name) + ' is a temporal table, with rows versioned in ' @@ -3704,13 +5719,105 @@ BEGIN; '' AS index_definition, 'N/A' AS secret_columns, 'N/A' AS index_usage_summary, - 'N/A' AS index_size_summary - FROM #TemporalTables AS t; + '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; + INSERT #BlitzIndexResults ( check_id, Priority, findings_group, finding, [database_name], URL, details, index_definition, + secret_columns, index_usage_summary, index_size_summary ) + + SELECT 121 AS check_id, + 200 AS Priority, + 'Specialized Indexes' AS findings_group, + 'Optimized For Sequential Keys', + i.database_name, + '' AS URL, + 'The table ' + QUOTENAME(i.schema_name) + '.' + QUOTENAME(i.object_name) + ' is optimized for sequential keys.' AS details, + '' AS index_definition, + 'N/A' AS secret_columns, + 'N/A' AS index_usage_summary, + 'N/A' AS index_size_summary + FROM #IndexSanity AS i + WHERE i.optimize_for_sequential_key = 1 + 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 */ - END; - + + + + + + RAISERROR(N'Insert a row to help people find help', 0,1) WITH NOWAIT; IF DATEDIFF(MM, @VersionDate, GETDATE()) > 6 BEGIN @@ -3719,7 +5826,7 @@ BEGIN; VALUES ( -1, 0 , 'Outdated sp_BlitzIndex', 'sp_BlitzIndex is Over 6 Months Old', 'http://FirstResponderKit.org/', 'Fine wine gets better with age, but this ' + @ScriptVersionName + ' is more like bad cheese. Time to get a new one.', - N'',N'',N'' + @DaysUptimeInsertValue,N'',N'' ); END; @@ -3731,8 +5838,7 @@ BEGIN; @ScriptVersionName, CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' + @DaysUptimeInsertValue,N'',N'' ); END; ELSE IF @Mode = 0 OR (@GetAllDatabases = 1 AND @Mode <> 4) @@ -3743,15 +5849,17 @@ BEGIN; @ScriptVersionName, CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , - N'' - , N'',N'' + @DaysUptimeInsertValue, N'',N'' ); INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, index_usage_summary, index_size_summary ) VALUES ( 1, 0 , - 'No Major Problems Found', - 'Nice Work!', - 'http://FirstResponderKit.org', 'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', 'The new default Mode 0 only looks for very serious index issues.', '', '' + N'No Major Problems Found', + N'Nice Work!', + N'http://FirstResponderKit.org', + N'Consider running with @Mode = 4 in individual databases (not all) for more detailed diagnostics.', + N'The default Mode 0 only looks for very serious index issues.', + @DaysUptimeInsertValue, N'' ); END; @@ -3762,16 +5870,16 @@ BEGIN; VALUES ( -1, 0 , @ScriptVersionName, CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + QUOTENAME(@DatabaseName) + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - N'From Your Community Volunteers' , N'http://www.BrentOzar.com/BlitzIndex' , - N'' - , N'',N'' + N'From Your Community Volunteers' , N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, N'',N'' ); INSERT #BlitzIndexResults ( Priority, check_id, findings_group, finding, URL, details, index_definition, index_usage_summary, index_size_summary ) VALUES ( 1, 0 , - 'No Problems Found', - 'Nice job! Or more likely, you have a nearly empty database.', - 'http://FirstResponderKit.org', 'Time to go read some blog posts.', '', '', '' + N'No Problems Found', + N'Nice job! Or more likely, you have a nearly empty database.', + N'http://FirstResponderKit.org', 'Time to go read some blog posts.', + @DaysUptimeInsertValue, N'', N'' ); END; @@ -3779,221 +5887,424 @@ BEGIN; RAISERROR(N'Returning results.', 0,1) WITH NOWAIT; /*Return results.*/ - IF (@Mode = 0) - BEGIN - - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - WHERE br.check_id IN (0, 1, 11, 22, 43, 68, 50, 60, 61, 62, 63, 64, 65, 72) - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END; - ELSE IF (@Mode = 4) - SELECT Priority, ISNULL(br.findings_group,N'') + - CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END - + br.finding AS [Finding], - br.[database_name] AS [Database Name], - br.details AS [Details: schema.table.index(indexid)], - br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(br.secret_columns,'') AS [Secret Columns], - br.index_usage_summary AS [Usage], - br.index_size_summary AS [Size], - COALESCE(br.more_info,sn.more_info,'') AS [More Info], - br.URL, - COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL] - FROM #BlitzIndexResults br - LEFT JOIN #IndexSanity sn ON - br.index_sanity_id=sn.index_sanity_id - LEFT JOIN #IndexCreateTsql ts ON - br.index_sanity_id=ts.index_sanity_id - ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC - OPTION (RECOMPILE); - - END; /* End @Mode=0 or 4 (diagnose)*/ - ELSE IF @Mode=1 /*Summarize*/ - BEGIN - --This mode is to give some overall stats on the database. - RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; - - SELECT DB_NAME(i.database_id) AS [Database Name], - CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], - CAST(CAST(SUM(sz.total_reserved_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], - CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], - CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ - 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], - CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], - CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], - SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], - CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], - CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN - CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) - ELSE 0 END AS [ratio table: NC Indexes], - SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], - CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Heaps GB], - SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], - SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], - CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], - SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], - SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], - MAX(total_rows) AS [Max Row Count], - CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max Table GB], - CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) - /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], - SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], - SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], - MIN(create_date) AS [Oldest Create Date], - MAX(create_date) AS [Most Recent Create Date], - MAX(modify_date) AS [Most Recent Modify Date], - 1 AS [Display Order] - FROM #IndexSanity AS i - --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz - ON i.index_sanity_id=sz.index_sanity_id - GROUP BY DB_NAME(i.database_id) - UNION ALL - SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL,NULL,0 AS display_order - ORDER BY [Display Order] ASC - OPTION (RECOMPILE); - - END; /* End @Mode=1 (summarize)*/ - ELSE IF @Mode=2 /*Index Detail*/ - BEGIN - --This mode just spits out all the detail without filters. - --This supports slicing AND dicing in Excel - RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; - - - /* Checks if @OutputServerName is populated with a valid linked server, and that the database name specified is valid */ - DECLARE @ValidOutputServer BIT; - DECLARE @ValidOutputLocation BIT; - DECLARE @LinkedServerDBCheck NVARCHAR(2000); - DECLARE @ValidLinkedServerDB INT; - DECLARE @tmpdbchk TABLE (cnt INT); - DECLARE @StringToExecute NVARCHAR(MAX); - - IF @OutputServerName IS NOT NULL + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) BEGIN - IF (SUBSTRING(@OutputTableName, 2, 1) = '#') + IF NOT @SchemaExists = 1 BEGIN - RAISERROR('Due to the nature of temporary tables, outputting to a linked server requires a permanent table.', 16, 0); - END; - ELSE IF EXISTS (SELECT server_id FROM sys.servers WHERE QUOTENAME([name]) = @OutputServerName) + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 BEGIN - SET @LinkedServerDBCheck = 'SELECT 1 WHERE EXISTS (SELECT * FROM '+@OutputServerName+'.master.sys.databases WHERE QUOTENAME([name]) = '''+@OutputDatabaseName+''')'; - INSERT INTO @tmpdbchk EXEC sys.sp_executesql @LinkedServerDBCheck; - SET @ValidLinkedServerDB = (SELECT COUNT(*) FROM @tmpdbchk); - IF (@ValidLinkedServerDB > 0) + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [priority] INT, + [finding] NVARCHAR(4000), + [database_name] NVARCHAR(128), + [details] NVARCHAR(MAX), + [index_definition] NVARCHAR(MAX), + [secret_columns] NVARCHAR(MAX), + [index_usage_summary] NVARCHAR(MAX), + [index_size_summary] NVARCHAR(MAX), + [more_info] NVARCHAR(MAX), + [url] NVARCHAR(MAX), + [create_tsql] NVARCHAR(MAX), + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 BEGIN - SET @ValidOutputServer = 1; - SET @ValidOutputLocation = 1; - END; + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; ELSE - RAISERROR('The specified database was not found on the output server', 16, 0); - END; - ELSE - BEGIN - RAISERROR('The specified output server was not found', 16, 0); - END; - END; - ELSE - BEGIN - IF (SUBSTRING(@OutputTableName, 2, 2) = '##') - BEGIN - SET @StringToExecute = N' IF (OBJECT_ID(''[tempdb].[dbo].@@@OutputTableName@@@'') IS NOT NULL) DROP TABLE @@@OutputTableName@@@'; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); - EXEC(@StringToExecute); + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); - SET @OutputDatabaseName = '[tempdb]'; - SET @OutputSchemaName = '[dbo]'; - SET @ValidOutputLocation = 1; - END; - ELSE IF (SUBSTRING(@OutputTableName, 2, 1) = '#') - BEGIN - RAISERROR('Due to the nature of Dymamic SQL, only global (i.e. double pound (##)) temp tables are supported for @OutputTableName', 16, 0); - END; - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - SET @ValidOutputLocation = 1; - SET @OutputServerName = QUOTENAME(CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); - END; - ELSE IF @OutputDatabaseName IS NOT NULL - AND @OutputSchemaName IS NOT NULL - AND @OutputTableName IS NOT NULL - AND NOT EXISTS ( SELECT * - FROM sys.databases - WHERE QUOTENAME([name]) = @OutputDatabaseName) - BEGIN - RAISERROR('The specified output database was not found on this server', 16, 0); - END; - ELSE + IF NOT @TableExists = 1 BEGIN - SET @ValidOutputLocation = 0; + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; END; - END; - /* @OutputTableName lets us export the results to a permanent table */ - DECLARE @RunID UNIQUEIDENTIFIER; - SET @RunID = NEWID(); - - IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) - BEGIN - DECLARE @TableExists BIT; - DECLARE @SchemaExists BIT; SET @StringToExecute = - N'SET @SchemaExists = 0; - SET @TableExists = 0; - IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - SET @SchemaExists = 1 - IF EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 1'; + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [priority], + [finding], + [database_name], + [details], + [index_definition], + [secret_columns], + [index_usage_summary], + [index_size_summary], + [more_info], + [url], + [create_tsql], + [sample_query_plan] + ) + SELECT + ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + Priority, ISNULL(br.findings_group,N'''') + + CASE WHEN ISNULL(br.finding,N'''') <> N'''' THEN N'': '' ELSE N'''' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'''') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'''') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'''') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE);'; SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + SELECT Priority, ISNULL(br.findings_group,N'') + + CASE WHEN ISNULL(br.finding,N'') <> N'' THEN N': ' ELSE N'' END + + br.finding AS [Finding], + br.[database_name] AS [Database Name], + br.details AS [Details: schema.table.index(indexid)], + br.index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(br.secret_columns,'') AS [Secret Columns], + br.index_usage_summary AS [Usage], + br.index_size_summary AS [Size], + COALESCE(br.more_info,sn.more_info,'') AS [More Info], + br.URL, + COALESCE(br.create_tsql,ts.create_tsql,'') AS [Create TSQL], + br.sample_query_plan AS [Sample Query Plan] + FROM #BlitzIndexResults br + LEFT JOIN #IndexSanity sn ON + br.index_sanity_id=sn.index_sanity_id + LEFT JOIN #IndexCreateTsql ts ON + br.index_sanity_id=ts.index_sanity_id + ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC + OPTION (RECOMPILE); + END; + + END; + + END /* End @Mode=0 or 4 (diagnose)*/ + + + + + + + + + ELSE IF (@Mode=1) /*Summarize*/ + BEGIN + --This mode is to give some overall stats on the database. + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [object_count] INT, + [reserved_gb] NUMERIC(29,1), + [reserved_lob_gb] NUMERIC(29,1), + [reserved_row_overflow_gb] NUMERIC(29,1), + [clustered_table_count] INT, + [clustered_table_gb] NUMERIC(29,1), + [nc_index_count] INT, + [nc_index_gb] NUMERIC(29,1), + [table_nc_index_ratio] NUMERIC(29,1), + [heap_count] INT, + [heap_gb] NUMERIC(29,1), + [partitioned_table_count] INT, + [partitioned_nc_count] INT, + [partitioned_gb] NUMERIC(29,1), + [filtered_index_count] INT, + [indexed_view_count] INT, + [max_table_row_count] INT, + [max_table_gb] NUMERIC(29,1), + [max_nc_index_gb] NUMERIC(29,1), + [table_count_over_1gb] INT, + [table_count_over_10gb] INT, + [table_count_over_100gb] INT, + [nc_index_count_over_1gb] INT, + [nc_index_count_over_10gb] INT, + [nc_index_count_over_100gb] INT, + [min_create_date] DATETIME, + [max_create_date] DATETIME, + [max_modify_date] DATETIME, + [display_order] INT, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + + SET @StringToExecute = + N'INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [object_count], + [reserved_gb], + [reserved_lob_gb], + [reserved_row_overflow_gb], + [clustered_table_count], + [clustered_table_gb], + [nc_index_count], + [nc_index_gb], + [table_nc_index_ratio], + [heap_count], + [heap_gb], + [partitioned_table_count], + [partitioned_nc_count], + [partitioned_gb], + [filtered_index_count], + [indexed_view_count], + [max_table_row_count], + [max_table_gb], + [max_nc_index_gb], + [table_count_over_1gb], + [table_count_over_10gb], + [table_count_over_100gb], + [nc_index_count_over_1gb], + [nc_index_count_over_10gb], + [nc_index_count_over_100gb], + [min_create_date], + [max_create_date], + [max_modify_date], + [display_order] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! initial columns are not casted to nvarchar due to not outputing informational line + DB_NAME(i.database_id) AS [Database Name], + COUNT(*) AS [Number Objects], + CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS [All GB], + CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS [LOB GB], + CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS [Row Overflow GB], + SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '''' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don''t lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + ORDER BY [Display Order] ASC + OPTION (RECOMPILE);'; - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT, @SchemaExists BIT OUTPUT', @TableExists OUTPUT, @SchemaExists OUTPUT; - + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC(@StringToExecute); + + END; /* @ValidOutputLocation = 1 */ + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + + RAISERROR(N'@Mode=1, we are summarizing.', 0,1) WITH NOWAIT; + + SELECT DB_NAME(i.database_id) AS [Database Name], + CAST((COUNT(*)) AS NVARCHAR(256)) AS [Number Objects], + CAST(CAST(SUM(sz.total_reserved_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [All GB], + CAST(CAST(SUM(sz.total_reserved_LOB_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [LOB GB], + CAST(CAST(SUM(sz.total_reserved_row_overflow_MB)/ + 1024. AS NUMERIC(29,1)) AS NVARCHAR(500)) AS [Row Overflow GB], + CAST(SUM(CASE WHEN index_id=1 THEN 1 ELSE 0 END)AS NVARCHAR(50)) AS [Clustered Tables], + CAST(SUM(CASE WHEN index_id=1 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Clustered Tables GB], + SUM(CASE WHEN index_id NOT IN (0,1) THEN 1 ELSE 0 END) AS [NC Indexes], + CAST(SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [NC Indexes GB], + CASE WHEN SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) > 0 THEN + CAST(SUM(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + / SUM(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) AS NUMERIC(29,1)) + ELSE 0 END AS [ratio table: NC Indexes], + SUM(CASE WHEN index_id=0 THEN 1 ELSE 0 END) AS [Heaps], + CAST(SUM(CASE WHEN index_id=0 THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Heaps GB], + SUM(CASE WHEN index_id IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned Tables], + SUM(CASE WHEN index_id NOT IN (0,1) AND partition_key_column_name IS NOT NULL THEN 1 ELSE 0 END) AS [Partitioned NCs], + CAST(SUM(CASE WHEN partition_key_column_name IS NOT NULL THEN sz.total_reserved_MB ELSE 0 END)/1024. AS NUMERIC(29,1)) AS [Partitioned GB], + SUM(CASE WHEN filter_definition <> '' THEN 1 ELSE 0 END) AS [Filtered Indexes], + SUM(CASE WHEN is_indexed_view=1 THEN 1 ELSE 0 END) AS [Indexed Views], + MAX(total_rows) AS [Max Row Count], + CAST(MAX(CASE WHEN index_id IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max Table GB], + CAST(MAX(CASE WHEN index_id NOT IN (0,1) THEN sz.total_reserved_MB ELSE 0 END) + /1024. AS NUMERIC(29,1)) AS [Max NC Index GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count Tables > 1GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count Tables > 10GB], + SUM(CASE WHEN index_id IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count Tables > 100GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 1024 THEN 1 ELSE 0 END) AS [Count NCs > 1GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 10240 THEN 1 ELSE 0 END) AS [Count NCs > 10GB], + SUM(CASE WHEN index_id NOT IN (0,1) AND sz.total_reserved_MB > 102400 THEN 1 ELSE 0 END) AS [Count NCs > 100GB], + MIN(create_date) AS [Oldest Create Date], + MAX(create_date) AS [Most Recent Create Date], + MAX(modify_date) AS [Most Recent Modify Date], + 1 AS [Display Order] + FROM #IndexSanity AS i + --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz + ON i.index_sanity_id=sz.index_sanity_id + GROUP BY DB_NAME(i.database_id) + UNION ALL + SELECT CASE WHEN @GetAllDatabases = 1 THEN N'All Databases' ELSE N'Database ' + N' as of ' + CONVERT(NVARCHAR(16),GETDATE(),121) END, + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL,NULL,0 AS display_order + ORDER BY [Display Order] ASC + OPTION (RECOMPILE); + END; + END; + + END; /* End @Mode=1 (summarize)*/ + + + + + + + + + ELSE IF (@Mode=2) /*Index Detail*/ + BEGIN + --This mode just spits out all the detail without filters. + --This supports slicing AND dicing in Excel + RAISERROR(N'@Mode=2, here''s the details on existing indexes.', 0,1) WITH NOWAIT; + + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN IF @SchemaExists = 1 BEGIN IF @TableExists = 0 @@ -4008,11 +6319,13 @@ BEGIN; [database_name] NVARCHAR(128), [schema_name] NVARCHAR(128), [table_name] NVARCHAR(128), - [index_name] NVARCHAR(128), + [index_name] NVARCHAR(128), + [Drop_Tsql] NVARCHAR(MAX), + [Create_Tsql] NVARCHAR(MAX), [index_id] INT, [db_schema_object_indexid] NVARCHAR(500), [object_type] NVARCHAR(15), - [index_definition] NVARCHAR(4000), + [index_definition] NVARCHAR(MAX), [key_column_names_with_sort_order] NVARCHAR(MAX), [count_key_columns] INT, [include_column_names] NVARCHAR(MAX), @@ -4022,11 +6335,13 @@ BEGIN; [partition_key_column_name] NVARCHAR(MAX), [filter_definition] NVARCHAR(MAX), [is_indexed_view] BIT, - [is_primary_key] BIT, + [is_primary_key] BIT, + [is_unique_constraint] BIT, [is_XML] BIT, [is_spatial] BIT, [is_NC_columnstore] BIT, [is_CX_columnstore] BIT, + [is_in_memory_oltp] BIT, [is_disabled] BIT, [is_hypothetical] BIT, [is_padded] BIT, @@ -4040,6 +6355,11 @@ BEGIN; [user_updates] BIGINT, [reads_per_write] MONEY, [index_usage_summary] NVARCHAR(200), + [total_singleton_lookup_count] BIGINT, + [total_range_scan_count] BIGINT, + [total_leaf_delete_count] BIGINT, + [total_leaf_update_count] BIGINT, + [index_op_stats] NVARCHAR(200), [partition_count] INT, [total_rows] BIGINT, [total_reserved_MB] NUMERIC(29,2), @@ -4056,7 +6376,12 @@ BEGIN; [avg_page_lock_wait_in_ms] BIGINT, [total_index_lock_promotion_attempt_count] BIGINT, [total_index_lock_promotion_count] BIGINT, - [data_compression_desc] VARCHAR(8000), + [total_forwarded_fetch_count] BIGINT, + [data_compression_desc] NVARCHAR(4000), + [page_latch_wait_count] BIGINT, + [page_latch_wait_in_ms] BIGINT, + [page_io_latch_wait_count] BIGINT, + [page_io_latch_wait_in_ms] BIGINT, [create_date] DATETIME, [modify_date] DATETIME, [more_info] NVARCHAR(500), @@ -4080,20 +6405,10 @@ BEGIN; END; END; /* @TableExists = 0 */ - SET @StringToExecute = - N'IF EXISTS(SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''@@@OutputSchemaName@@@'') - AND NOT EXISTS (SELECT * FROM @@@OutputServerName@@@.@@@OutputDatabaseName@@@.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''@@@OutputSchemaName@@@'' AND QUOTENAME(TABLE_NAME) = ''@@@OutputTableName@@@'') - SET @TableExists = 0 - ELSE - SET @TableExists = 1'; - + + -- Re-check that table now exists (if not we failed creating it) SET @TableExists = NULL; - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); - SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); - - EXEC sp_executesql @StringToExecute, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; IF @TableExists = 1 BEGIN @@ -4106,7 +6421,9 @@ BEGIN; [database_name], [schema_name], [table_name], - [index_name], + [index_name], + [Drop_Tsql], + [Create_Tsql], [index_id], [db_schema_object_indexid], [object_type], @@ -4120,11 +6437,13 @@ BEGIN; [partition_key_column_name], [filter_definition], [is_indexed_view], - [is_primary_key], + [is_primary_key], + [is_unique_constraint], [is_XML], [is_spatial], [is_NC_columnstore], [is_CX_columnstore], + [is_in_memory_oltp], [is_disabled], [is_hypothetical], [is_padded], @@ -4138,6 +6457,11 @@ BEGIN; [user_updates], [reads_per_write], [index_usage_summary], + [total_singleton_lookup_count], + [total_range_scan_count], + [total_leaf_delete_count], + [total_leaf_update_count], + [index_op_stats], [partition_count], [total_rows], [total_reserved_MB], @@ -4154,7 +6478,12 @@ BEGIN; [avg_page_lock_wait_in_ms], [total_index_lock_promotion_attempt_count], [total_index_lock_promotion_count], + [total_forwarded_fetch_count], [data_compression_desc], + [page_latch_wait_count], + [page_latch_wait_in_ms], + [page_io_latch_wait_count], + [page_io_latch_wait_in_ms], [create_date], [modify_date], [more_info], @@ -4168,13 +6497,28 @@ BEGIN; i.[database_name] AS [Database Name], i.[schema_name] AS [Schema Name], i.[object_name] AS [Object Name], - ISNULL(i.index_name, '''') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], + ISNULL(i.index_name, '''') AS [Index Name], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> ''[HEAP]'' + THEN N''--ALTER TABLE '' + QUOTENAME(i.[database_name]) + N''.'' + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + + N'' DROP CONSTRAINT '' + QUOTENAME(i.index_name) + N'';'' + WHEN i.is_primary_key = 0 AND i.index_definition <> ''[HEAP]'' + THEN N''--DROP INDEX ''+ QUOTENAME(i.index_name) + N'' ON '' + QUOTENAME(i.[database_name]) + N''.'' + + QUOTENAME(i.[schema_name]) + N''.'' + QUOTENAME(i.[object_name]) + N'';'' + ELSE N'''' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = ''[HEAP]'' THEN N'''' + ELSE N''--'' + ict.create_tsql END AS [Create TSQL], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], db_schema_object_indexid AS [Details: schema.table.index(indexid)], CASE WHEN index_id IN ( 1, 0 ) THEN ''TABLE'' ELSE ''NonClustered'' END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + LEFT(index_definition,4000) AS [Definition: [Property]] ColumnName {datatype maxbytes}], ISNULL(LTRIM(key_column_names_with_sort_order), '''') AS [Key Column Names With Sort], ISNULL(count_key_columns, 0) AS [Count Key Columns], ISNULL(include_column_names, '''') AS [Include Column Names], @@ -4185,10 +6529,12 @@ BEGIN; ISNULL(filter_definition, '''') AS [Filter Definition], is_indexed_view AS [Is Indexed View], is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint], is_XML AS [Is XML], is_spatial AS [Is Spatial], is_NC_columnstore AS [Is NC Columnstore], is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], is_disabled AS [Is Disabled], is_hypothetical AS [Is Hypothetical], is_padded AS [Is Padded], @@ -4202,6 +6548,11 @@ BEGIN; user_updates AS [User Updates], reads_per_write AS [Reads Per Write], index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], sz.partition_count AS [Partition Count], sz.total_rows AS [Rows], sz.total_reserved_MB AS [Reserved MB], @@ -4218,13 +6569,19 @@ BEGIN; sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], sz.data_compression_desc AS [Data Compression], + sz.page_latch_wait_count, + sz.page_latch_wait_in_ms, + sz.page_io_latch_wait_count, + sz.page_io_latch_wait_in_ms, i.create_date AS [Create Date], i.modify_date AS [Modify Date], more_info AS [More Info], 1 AS [Display Order] FROM #IndexSanity AS i LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] OPTION (RECOMPILE);'; @@ -4245,131 +6602,401 @@ BEGIN; END; /* @ValidOutputLocation = 1 */ ELSE + IF(@OutputType <> 'NONE') + BEGIN + SELECT i.[database_name] AS [Database Name], + i.[schema_name] AS [Schema Name], + i.[object_name] AS [Object Name], + ISNULL(i.index_name, '') AS [Index Name], + CAST(i.index_id AS NVARCHAR(10))AS [Index ID], + db_schema_object_indexid AS [Details: schema.table.index(indexid)], + CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' + ELSE 'NonClustered' + END AS [Object Type], + index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], + ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], + ISNULL(count_key_columns, 0) AS [Count Key Columns], + ISNULL(include_column_names, '') AS [Include Column Names], + ISNULL(count_included_columns,0) AS [Count Included Columns], + ISNULL(secret_columns,'') AS [Secret Column Names], + ISNULL(count_secret_columns,0) AS [Count Secret Columns], + ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], + ISNULL(filter_definition, '') AS [Filter Definition], + is_indexed_view AS [Is Indexed View], + is_primary_key AS [Is Primary Key], + is_unique_constraint AS [Is Unique Constraint] , + is_XML AS [Is XML], + is_spatial AS [Is Spatial], + is_NC_columnstore AS [Is NC Columnstore], + is_CX_columnstore AS [Is CX Columnstore], + is_in_memory_oltp AS [Is In-Memory OLTP], + is_disabled AS [Is Disabled], + is_hypothetical AS [Is Hypothetical], + is_padded AS [Is Padded], + fill_factor AS [Fill Factor], + is_referenced_by_foreign_key AS [Is Reference by Foreign Key], + last_user_seek AS [Last User Seek], + last_user_scan AS [Last User Scan], + last_user_lookup AS [Last User Lookup], + last_user_update AS [Last User Update], + total_reads AS [Total Reads], + user_updates AS [User Updates], + reads_per_write AS [Reads Per Write], + index_usage_summary AS [Index Usage], + sz.total_singleton_lookup_count AS [Singleton Lookups], + sz.total_range_scan_count AS [Range Scans], + sz.total_leaf_delete_count AS [Leaf Deletes], + sz.total_leaf_update_count AS [Leaf Updates], + sz.index_op_stats AS [Index Op Stats], + sz.partition_count AS [Partition Count], + sz.total_rows AS [Rows], + sz.total_reserved_MB AS [Reserved MB], + sz.total_reserved_LOB_MB AS [Reserved LOB MB], + sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], + sz.index_size_summary AS [Index Size], + sz.total_row_lock_count AS [Row Lock Count], + sz.total_row_lock_wait_count AS [Row Lock Wait Count], + sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], + sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], + sz.total_page_lock_count AS [Page Lock Count], + sz.total_page_lock_wait_count AS [Page Lock Wait Count], + sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], + sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], + sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], + sz.total_index_lock_promotion_count AS [Lock Escalations], + sz.page_latch_wait_count AS [Page Latch Wait Count], + sz.page_latch_wait_in_ms AS [Page Latch Wait ms], + sz.page_io_latch_wait_count AS [Page IO Latch Wait Count], + sz.page_io_latch_wait_in_ms as [Page IO Latch Wait ms], + sz.total_forwarded_fetch_count AS [Forwarded Fetches], + sz.data_compression_desc AS [Data Compression], + i.create_date AS [Create Date], + i.modify_date AS [Modify Date], + more_info AS [More Info], + CASE + WHEN i.is_primary_key = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.is_unique_constraint = 1 AND i.index_definition <> '[HEAP]' + THEN N'--ALTER TABLE ' + QUOTENAME(i.[database_name]) + N'.' + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + + N' DROP CONSTRAINT ' + QUOTENAME(i.index_name) + N';' + WHEN i.is_primary_key = 0 AND i.index_definition <> '[HEAP]' + THEN N'--DROP INDEX '+ QUOTENAME(i.index_name) + N' ON ' + QUOTENAME(i.[database_name]) + N'.' + + QUOTENAME(i.[schema_name]) + N'.' + QUOTENAME(i.[object_name]) + N';' + ELSE N'' + END AS [Drop TSQL], + CASE + WHEN i.index_definition = '[HEAP]' THEN N'' + ELSE N'--' + ict.create_tsql END AS [Create TSQL], + 1 AS [Display Order] + INTO #Mode2Temp + FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes + LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id + LEFT JOIN #IndexCreateTsql AS ict ON i.index_sanity_id = ict.index_sanity_id + OPTION(RECOMPILE); + + IF @@ROWCOUNT > 0 + BEGIN + SELECT + sz.* + FROM #Mode2Temp AS sz + ORDER BY /* Shout out to DHutmacher */ + /*DESC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'desc' THEN sz.[Rows] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'desc' THEN sz.[Reserved MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'desc' THEN sz.[Reserved LOB MB] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'desc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'desc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'desc' THEN [Total Reads] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'desc' THEN [User Updates] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'desc' THEN [Reads Per Write] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'desc' THEN sz.[Forwarded Fetches] ELSE NULL END DESC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END DESC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'desc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END DESC, + /*ASC*/ + CASE WHEN @SortOrder = N'rows' AND @SortDirection = N'asc' THEN sz.[Rows] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_mb' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'size' AND @SortDirection = N'asc' THEN sz.[Reserved MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reserved_lob_mb' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lob' AND @SortDirection = N'asc' THEN sz.[Reserved LOB MB] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_row_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Row Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_page_lock_wait_in_ms' AND @SortDirection = N'asc' THEN COALESCE(sz.[Page Lock Wait ms],0) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'lock_time' AND @SortDirection = N'asc' THEN (COALESCE(sz.[Row Lock Wait ms],0) + COALESCE(sz.[Page Lock Wait ms],0)) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'total_reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads' AND @SortDirection = N'asc' THEN [Total Reads] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'user_updates' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'writes' AND @SortDirection = N'asc' THEN [User Updates] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'reads_per_write' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'ratio' AND @SortDirection = N'asc' THEN [Reads Per Write] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'forward_fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'fetches' AND @SortDirection = N'asc' THEN sz.[Forwarded Fetches] ELSE NULL END ASC, + CASE WHEN @SortOrder = N'create_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Create Date]) ELSE NULL END ASC, + CASE WHEN @SortOrder = N'modify_date' AND @SortDirection = N'asc' THEN CONVERT(DATETIME, sz.[Modify Date]) ELSE NULL END ASC, + sz.[Database Name], [Schema Name], [Object Name], [Index ID] + OPTION (RECOMPILE); + END + ELSE + BEGIN + SELECT + DatabaseDetails = + N'Database ' + + ISNULL(@DatabaseName, DB_NAME()) + + N' has ' + + ISNULL(RTRIM(@Rowcount), 0) + + N' partitions.', + BringThePain = + CASE + WHEN @BringThePain IN (0, 1) AND ISNULL(@Rowcount, 0) = 0 + THEN N'Check the database name, it looks like nothing is here.' + WHEN @BringThePain = 0 AND ISNULL(@Rowcount, 0) > 0 + THEN N'Please re-run with @BringThePain = 1' + END; + END + END; + END; /* End @Mode=2 (index detail)*/ - SELECT i.[database_name] AS [Database Name], - i.[schema_name] AS [Schema Name], - i.[object_name] AS [Object Name], - ISNULL(i.index_name, '') AS [Index Name], - CAST(i.index_id AS VARCHAR(10))AS [Index ID], - db_schema_object_indexid AS [Details: schema.table.index(indexid)], - CASE WHEN index_id IN ( 1, 0 ) THEN 'TABLE' - ELSE 'NonClustered' - END AS [Object Type], - index_definition AS [Definition: [Property]] ColumnName {datatype maxbytes}], - ISNULL(LTRIM(key_column_names_with_sort_order), '') AS [Key Column Names With Sort], - ISNULL(count_key_columns, 0) AS [Count Key Columns], - ISNULL(include_column_names, '') AS [Include Column Names], - ISNULL(count_included_columns,0) AS [Count Included Columns], - ISNULL(secret_columns,'') AS [Secret Column Names], - ISNULL(count_secret_columns,0) AS [Count Secret Columns], - ISNULL(partition_key_column_name, '') AS [Partition Key Column Name], - ISNULL(filter_definition, '') AS [Filter Definition], - is_indexed_view AS [Is Indexed View], - is_primary_key AS [Is Primary Key], - is_XML AS [Is XML], - is_spatial AS [Is Spatial], - is_NC_columnstore AS [Is NC Columnstore], - is_CX_columnstore AS [Is CX Columnstore], - is_disabled AS [Is Disabled], - is_hypothetical AS [Is Hypothetical], - is_padded AS [Is Padded], - fill_factor AS [Fill Factor], - is_referenced_by_foreign_key AS [Is Reference by Foreign Key], - last_user_seek AS [Last User Seek], - last_user_scan AS [Last User Scan], - last_user_lookup AS [Last User Lookup], - last_user_update AS [Last User Update], - total_reads AS [Total Reads], - user_updates AS [User Updates], - reads_per_write AS [Reads Per Write], - index_usage_summary AS [Index Usage], - sz.partition_count AS [Partition Count], - sz.total_rows AS [Rows], - sz.total_reserved_MB AS [Reserved MB], - sz.total_reserved_LOB_MB AS [Reserved LOB MB], - sz.total_reserved_row_overflow_MB AS [Reserved Row Overflow MB], - sz.index_size_summary AS [Index Size], - sz.total_row_lock_count AS [Row Lock Count], - sz.total_row_lock_wait_count AS [Row Lock Wait Count], - sz.total_row_lock_wait_in_ms AS [Row Lock Wait ms], - sz.avg_row_lock_wait_in_ms AS [Avg Row Lock Wait ms], - sz.total_page_lock_count AS [Page Lock Count], - sz.total_page_lock_wait_count AS [Page Lock Wait Count], - sz.total_page_lock_wait_in_ms AS [Page Lock Wait ms], - sz.avg_page_lock_wait_in_ms AS [Avg Page Lock Wait ms], - sz.total_index_lock_promotion_attempt_count AS [Lock Escalation Attempts], - sz.total_index_lock_promotion_count AS [Lock Escalations], - sz.data_compression_desc AS [Data Compression], - i.create_date AS [Create Date], - i.modify_date AS [Modify Date], - more_info AS [More Info], - 1 AS [Display Order] - FROM #IndexSanity AS i --left join here so we don't lose disabled nc indexes - LEFT JOIN #IndexSanitySize AS sz ON i.index_sanity_id = sz.index_sanity_id - ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] - OPTION (RECOMPILE); - END; /* End @Mode=2 (index detail)*/ - ELSE IF @Mode=3 /*Missing index Detail*/ + + + + + ELSE IF (@Mode=3) /*Missing index Detail*/ BEGIN + IF (@ValidOutputLocation = 1 AND COALESCE(@OutputServerName, @OutputDatabaseName, @OutputSchemaName, @OutputTableName) IS NOT NULL) + BEGIN + + IF NOT @SchemaExists = 1 + BEGIN + RAISERROR (N'Invalid schema name, data could not be saved.', 16, 0); + RETURN; + END + + IF @TableExists = 0 + BEGIN + SET @StringToExecute = + N'CREATE TABLE @@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [id] INT IDENTITY(1,1) NOT NULL, + [run_id] UNIQUEIDENTIFIER, + [run_datetime] DATETIME, + [server_name] NVARCHAR(128), + [database_name] NVARCHAR(128), + [schema_name] NVARCHAR(128), + [table_name] NVARCHAR(128), + [magic_benefit_number] BIGINT, + [missing_index_details] NVARCHAR(MAX), + [avg_total_user_cost] NUMERIC(29,4), + [avg_user_impact] NUMERIC(29,1), + [user_seeks] BIGINT, + [user_scans] BIGINT, + [unique_compiles] BIGINT, + [equality_columns_with_data_type] NVARCHAR(MAX), + [inequality_columns_with_data_type] NVARCHAR(MAX), + [included_columns_with_data_type] NVARCHAR(MAX), + [index_estimated_impact] NVARCHAR(256), + [create_tsql] NVARCHAR(MAX), + [more_info] NVARCHAR(600), + [display_order] INT, + [is_low] BIT, + [sample_query_plan] XML, + CONSTRAINT [PK_ID_@@@RunID@@@] PRIMARY KEY CLUSTERED ([id] ASC) + );'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + + IF @ValidOutputServer = 1 + BEGIN + SET @StringToExecute = REPLACE(@StringToExecute,'''',''''''); + EXEC('EXEC('''+@StringToExecute+''') AT ' + @OutputServerName); + END; + ELSE + BEGIN + EXEC(@StringToExecute); + END; + END; /* @TableExists = 0 */ + + -- Re-check that table now exists (if not we failed creating it) + SET @TableExists = NULL; + EXEC sp_executesql @TableExistsSql, N'@TableExists BIT OUTPUT', @TableExists OUTPUT; + + IF NOT @TableExists = 1 + BEGIN + RAISERROR('Creation of the output table failed.', 16, 0); + RETURN; + END; + SET @StringToExecute = + N'WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + INSERT @@@OutputServerName@@@.@@@OutputDatabaseName@@@.@@@OutputSchemaName@@@.@@@OutputTableName@@@ + ( + [run_id], + [run_datetime], + [server_name], + [database_name], + [schema_name], + [table_name], + [magic_benefit_number], + [missing_index_details], + [avg_total_user_cost], + [avg_user_impact], + [user_seeks], + [user_scans], + [unique_compiles], + [equality_columns_with_data_type], + [inequality_columns_with_data_type], + [included_columns_with_data_type], + [index_estimated_impact], + [create_tsql], + [more_info], + [display_order], + [is_low], + [sample_query_plan] + ) + SELECT ''@@@RunID@@@'', + ''@@@GETDATE@@@'', + ''@@@LocalServerName@@@'', + -- Below should be a copy/paste of the real query + -- Make sure all quotes are escaped + -- NOTE! information line is skipped from output and the query below + -- NOTE! CTE block is above insert in the copied SQL + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE);'; + + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputServerName@@@', @OutputServerName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputDatabaseName@@@', @OutputDatabaseName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputSchemaName@@@', @OutputSchemaName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@OutputTableName@@@', @OutputTableName); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@RunID@@@', @RunID); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@GETDATE@@@', GETDATE()); + SET @StringToExecute = REPLACE(@StringToExecute, '@@@LocalServerName@@@', CAST(SERVERPROPERTY('ServerName') AS NVARCHAR(128))); + EXEC sp_executesql @StringToExecute, N'@DaysUptime NUMERIC(23,2), @ShowAllMissingIndexRequests BIT', @DaysUptime = @DaysUptime, @ShowAllMissingIndexRequests = @ShowAllMissingIndexRequests; + + END; /* @ValidOutputLocation = 1 */ + ELSE + BEGIN + IF(@OutputType <> 'NONE') + BEGIN + WITH create_date AS ( + SELECT i.database_id, + i.schema_name, + i.[object_id], + ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days + FROM #IndexSanity AS i + GROUP BY i.database_id, i.schema_name, i.object_id + ) + SELECT + mi.database_name AS [Database Name], + mi.[schema_name] AS [Schema], + mi.table_name AS [Table], + CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) + AS [Magic Benefit Number], + mi.missing_index_details AS [Missing Index Details], + mi.avg_total_user_cost AS [Avg Query Cost], + mi.avg_user_impact AS [Est Index Improvement], + mi.user_seeks AS [Seeks], + mi.user_scans AS [Scans], + mi.unique_compiles AS [Compiles], + mi.equality_columns_with_data_type AS [Equality Columns], + mi.inequality_columns_with_data_type AS [Inequality Columns], + mi.included_columns_with_data_type AS [Included Columns], + mi.index_estimated_impact AS [Estimated Impact], + mi.create_tsql AS [Create TSQL], + mi.more_info AS [More Info], + 1 AS [Display Order], + mi.is_low, + mi.sample_query_plan AS [Sample Query Plan] + FROM #MissingIndexes AS mi + LEFT JOIN create_date AS cd + ON mi.[object_id] = cd.object_id + AND mi.database_id = cd.database_id + AND mi.schema_name = cd.schema_name + /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ + WHERE @ShowAllMissingIndexRequests=1 + OR (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 + UNION ALL + SELECT + @ScriptVersionName, + N'From Your Community Volunteers' , + N'http://FirstResponderKit.org' , + 100000000000, + @DaysUptimeInsertValue, + NULL,NULL,NULL,NULL,NULL,NULL,NULL, + NULL, NULL, NULL, NULL, 0 AS [Display Order], NULL AS is_low, NULL + ORDER BY [Display Order] ASC, [Magic Benefit Number] DESC + OPTION (RECOMPILE); + END; + + + IF (@BringThePain = 1 + AND @DatabaseName IS NOT NULL + AND @GetAllDatabases = 0) + + BEGIN + EXEC sp_BlitzCache @SortOrder = 'sp_BlitzIndex', @DatabaseName = @DatabaseName, @BringThePain = 1, @QueryFilter = 'statement', @HideSummary = 1; + END; + + END; + + + - WITH create_date AS ( - SELECT i.database_id, - i.schema_name, - i.[object_id], - ISNULL(NULLIF(MAX(DATEDIFF(DAY, i.create_date, SYSDATETIME())), 0), 1) AS create_days - FROM #IndexSanity AS i - GROUP BY i.database_id, i.schema_name, i.object_id - ) - SELECT - mi.database_name AS [Database Name], - mi.[schema_name] AS [Schema], - mi.table_name AS [Table], - CAST((mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) AS BIGINT) - AS [Magic Benefit Number], - mi.missing_index_details AS [Missing Index Details], - mi.avg_total_user_cost AS [Avg Query Cost], - mi.avg_user_impact AS [Est Index Improvement], - mi.user_seeks AS [Seeks], - mi.user_scans AS [Scans], - mi.unique_compiles AS [Compiles], - mi.equality_columns AS [Equality Columns], - mi.inequality_columns AS [Inequality Columns], - mi.included_columns AS [Included Columns], - mi.index_estimated_impact AS [Estimated Impact], - mi.create_tsql AS [Create TSQL], - mi.more_info AS [More Info], - 1 AS [Display Order], - mi.is_low - FROM #MissingIndexes AS mi - LEFT JOIN create_date AS cd - ON mi.[object_id] = cd.object_id - AND mi.database_id = cd.database_id - AND mi.schema_name = cd.schema_name - /* Minimum benefit threshold = 100k/day of uptime OR since table creation date, whichever is lower*/ - WHERE (mi.magic_benefit_number / CASE WHEN cd.create_days < @DaysUptime THEN cd.create_days ELSE @DaysUptime END) >= 100000 - UNION ALL - SELECT - @ScriptVersionName, - N'From Your Community Volunteers' , - N'http://FirstResponderKit.org' , - 100000000000, - N'', - NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL, - NULL, 0 AS [Display Order], NULL AS is_low - ORDER BY [Display Order] ASC, is_low, [Magic Benefit Number] DESC - OPTION (RECOMPILE); END; /* End @Mode=3 (index detail)*/ -END; + SET @d = CONVERT(VARCHAR(19), GETDATE(), 121); + RAISERROR (N'finishing at %s',0,1, @d) WITH NOWAIT; +END /* End @TableName IS NULL (mode 0/1/2/3/4) */ END TRY BEGIN CATCH RAISERROR (N'Failure analyzing temp tables.', 0,1) WITH NOWAIT; - SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); + SELECT @msg = ERROR_MESSAGE(), @ErrorSeverity = ERROR_SEVERITY(), @ErrorState = ERROR_STATE(); RAISERROR (@msg, @ErrorSeverity, diff --git a/sp_BlitzLock.sql b/sp_BlitzLock.sql index 10246f955..53e923556 100644 --- a/sp_BlitzLock.sql +++ b/sp_BlitzLock.sql @@ -1,717 +1,4529 @@ IF OBJECT_ID('dbo.sp_BlitzLock') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); +BEGIN + EXECUTE ('CREATE PROCEDURE dbo.sp_BlitzLock AS RETURN 0;'); +END; GO -ALTER PROCEDURE dbo.sp_BlitzLock +ALTER PROCEDURE + dbo.sp_BlitzLock ( - @Top INT = 2147483647, - @DatabaseName NVARCHAR(256) = NULL, - @StartDate DATETIME = '19000101', - @EndDate DATETIME = '99991231', - @ObjectName NVARCHAR(1000) = NULL, - @StoredProcName NVARCHAR(1000) = NULL, - @AppName NVARCHAR(256) = NULL, - @HostName NVARCHAR(256) = NULL, - @LoginName NVARCHAR(256) = NULL, - @EventSessionPath VARCHAR(256) = 'system_health*.xel', - @Debug BIT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @DatabaseName sysname = NULL, + @StartDate datetime = NULL, + @EndDate datetime = NULL, + @ObjectName nvarchar(1024) = NULL, + @StoredProcName nvarchar(1024) = NULL, + @AppName sysname = NULL, + @HostName sysname = NULL, + @LoginName sysname = NULL, + @EventSessionName sysname = N'system_health', + @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, + @VersionDate datetime = NULL OUTPUT, + @VersionCheckMode bit = 0, + @OutputDatabaseName sysname = NULL, + @OutputSchemaName sysname = N'dbo', /*ditto as below*/ + @OutputTableName sysname = N'BlitzLock', /*put a standard here no need to check later in the script*/ + @ExportToExcel bit = 0 ) +WITH RECOMPILE AS BEGIN + SET STATISTICS XML OFF; + SET NOCOUNT ON; + SET XACT_ABORT OFF; + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF @VersionCheckMode = 1 + BEGIN + RETURN; + END; + + IF @Help = 1 + BEGIN + PRINT N' + /* + sp_BlitzLock from http://FirstResponderKit.org + + This script checks for and analyzes deadlocks from the system health session or a custom extended event path + + 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 + + @EndDate: The date you want to stop searching on, defaults to current date + + @ObjectName: If you want to filter to a specific able. + The object name has to be fully qualified ''Database.Schema.Table'' -SET NOCOUNT ON; -SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + @StoredProcName: If you want to search for a single stored proc + The proc name has to be fully qualified ''Database.Schema.Sproc'' -DECLARE @Version VARCHAR(30); -SET @Version = '1.0'; -SET @VersionDate = '20171201'; + @AppName: If you want to filter to a specific application + @HostName: If you want to filter to a specific host - IF @Help = 1 PRINT ' - /* - sp_BlitzLock from http://FirstResponderKit.org - - This script checks for and analyzes deadlocks from the system health session or a custom extended event path + @LoginName: If you want to filter to a specific login - Variables you can use: - @Top: Use if you want to limit the number of deadlocks to return. - This is ordered by event date ascending + @DeadlockType: Search for regular or parallel deadlocks specifically - @DatabaseName: If you want to filter to a specific database + /*Extended Event session details*/ + @EventSessionName: If you want to point this at an XE session rather than the system health session. - @StartDate: The date you want to start searching on. + @TargetSessionType: Can be ''ring_buffer'', ''event_file'', or ''table''. Leave NULL to auto-detect. - @EndDate: The date you want to stop searching on. + /*Output to a table*/ + @OutputDatabaseName: If you want to output information to a specific database - @ObjectName: If you want to filter to a specific able. - The object name has to be fully qualified ''Database.Schema.Table'' + @OutputSchemaName: Specify a schema name to output information to a specific Schema - @StoredProcName: If you want to search for a single stored proc - The proc name has to be fully qualified ''Database.Schema.Sproc'' - - @AppName: If you want to filter to a specific application - - @HostName: If you want to filter to a specific host - - @LoginName: If you want to filter to a specific login + @OutputTableName: Specify table name to to output information to a specific table - @EventSessionPath: If you want to point this at an XE session rather than the system health session. - - - - 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. + /*Point at a table containing deadlock XML*/ + @TargetDatabaseName: The database that contains the table with deadlock report XML - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) + @TargetSchemaName: The schema of the table containing deadlock report XML - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + @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. + + Known limitations of this version: + - Only SQL Server 2012 and newer is supported + - If your tables have weird characters in them (https://en.wikipedia.org/wiki/List_of_xml_and_HTML_character_entity_references) you may get errors trying to parse the xml. + I took a long look at this one, and: + 1) Trying to account for all the weird places these could crop up is a losing effort. + 2) Replace is slow af on lots of xml. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) MIT License - - All other copyright for sp_BlitzLock are held by Brent Ozar Unlimited, 2017. - Copyright (c) 2017 Brent Ozar Unlimited + Copyright (c) Brent Ozar Unlimited + + 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. + + */'; + + RETURN; + END; /* @Help = 1 */ + + /*Declare local variables used in the procudure*/ + DECLARE + @DatabaseId int = + DB_ID(@DatabaseName), + @ProductVersion nvarchar(128) = + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + @ProductVersionMajor float = + SUBSTRING + ( + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)), + 1, + CHARINDEX('.', CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128))) + 1 + ), + @ProductVersionMinor int = + PARSENAME + ( + CONVERT + ( + varchar(32), + CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(128)) + ), + 2 + ), + @ObjectFullName nvarchar(MAX) = N'', + @Azure bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 5 + THEN 1 + ELSE 0 + END, + @MI bit = + CASE + WHEN + ( + SELECT + CONVERT + ( + integer, + SERVERPROPERTY('EngineEdition') + ) + ) = 8 + THEN 1 + ELSE 0 + END, + @RDS bit = + CASE + WHEN LEFT(CAST(SERVERPROPERTY('ComputerNamePhysicalNetBIOS') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND LEFT(CAST(SERVERPROPERTY('MachineName') AS varchar(8000)), 8) <> 'EC2AMAZ-' + AND DB_ID('rdsadmin') IS NULL + THEN 0 + ELSE 1 + END, + @d varchar(40) = '', + @StringToExecute nvarchar(4000) = N'', + @StringToExecuteParams nvarchar(500) = N'', + @r sysname = NULL, + @OutputTableFindings nvarchar(100) = N'[BlitzLockFindings]', + @DeadlockCount int = 0, + @ServerName sysname = @@SERVERNAME, + @OutputDatabaseCheck bit = -1, + @SessionId int = 0, + @TargetSessionId int = 0, + @FileName nvarchar(4000) = N'', + @inputbuf_bom nvarchar(1) = CONVERT(nvarchar(1), 0x0a00, 0), + @deadlock_result nvarchar(MAX) = N'', + @StartDateOriginal datetime = @StartDate, + @EndDateOriginal datetime = @EndDate, + @StartDateUTC datetime, + @EndDateUTC datetime, + @extract_sql nvarchar(MAX), + @validation_sql nvarchar(MAX), + @xe bit, + @xd bit; + + /*Temporary objects used in the procedure*/ + DECLARE + @sysAssObjId AS table + ( + database_id int, + partition_id bigint, + schema_name sysname, + table_name sysname + ); + + CREATE TABLE + #x + ( + x xml NOT NULL + DEFAULT N'x' + ); + + CREATE TABLE + #deadlock_data + ( + deadlock_xml xml NOT NULL + DEFAULT N'x' + ); + + CREATE TABLE + #t + ( + id int NOT NULL + ); + + CREATE TABLE + #deadlock_findings + ( + id int IDENTITY PRIMARY KEY, + check_id int NOT NULL, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000), + sort_order bigint + ); + + /*Set these to some sane defaults if NULLs are passed in*/ + /*Normally I'd hate this, but we RECOMPILE everything*/ + + SELECT + @StartDate = + CASE + WHEN @StartDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + DATEADD + ( + DAY, + -7, + SYSDATETIME() + ) + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @StartDate + ) + END, + @EndDate = + CASE + WHEN @EndDate IS NULL + THEN + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + SYSDATETIME() + ) + ELSE + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + SYSDATETIME(), + GETUTCDATE() + ), + @EndDate + ) + END; + + SELECT + @StartDateUTC = @StartDate, + @EndDateUTC = @EndDate; + + IF + ( + @MI = 1 + AND @EventSessionName = N'system_health' + AND @TargetSessionType IS NULL + ) + BEGIN + SET + @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 + ( + SELECT + 1/0 + FROM sys.server_event_sessions AS ses + JOIN sys.dm_xe_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; + + IF @Azure = 1 + AND LOWER(@TargetSessionType) <> N'table' + BEGIN + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.database_event_sessions AS ses + JOIN sys.dm_xe_database_sessions AS dxs + ON dxs.name = ses.name + WHERE ses.name = @EventSessionName + AND dxs.create_time IS NOT NULL + ) + BEGIN + RAISERROR('A session with the name %s does not exist or is not currently active.', 11, 1, @EventSessionName) WITH NOWAIT; + RETURN; + END; + END; + + IF @OutputDatabaseName IS NOT NULL + BEGIN /*IF databaseName is set, do some sanity checks and put [] around def.*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@OutputDatabaseName set to %s, checking validity at %s', 0, 1, @OutputDatabaseName, @d) WITH NOWAIT; + + IF NOT EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name = @OutputDatabaseName + ) /*If database is invalid raiserror and set bitcheck*/ + BEGIN + RAISERROR('Database Name (%s) for output of table is invalid please, Output to Table will not be performed', 0, 1, @OutputDatabaseName) WITH NOWAIT; + SET @OutputDatabaseCheck = -1; /* -1 invalid/false, 0 = good/true */ + END; + ELSE + BEGIN + SET @OutputDatabaseCheck = 0; - 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: + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + 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 s.name =''' + + @OutputSchemaName + + N''';', + @StringToExecuteParams = + N'@r sysname OUTPUT'; - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; - 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. + IF @Debug = 1 + BEGIN + RAISERROR('@r is set to: %s for schema name %s and table name %s', 0, 1, @r, @OutputSchemaName, @OutputTableName) WITH NOWAIT; + END; + + /*protection spells*/ + SELECT + @ObjectFullName = + QUOTENAME(@OutputDatabaseName) + + N'.' + + QUOTENAME(@OutputSchemaName) + + N'.' + + QUOTENAME(@OutputTableName), + @OutputDatabaseName = + QUOTENAME(@OutputDatabaseName), + @OutputTableName = + QUOTENAME(@OutputTableName), + @OutputSchemaName = + QUOTENAME(@OutputSchemaName); + + IF (@r IS NOT NULL) /*if it is not null, there is a table, so check for newly added columns*/ + BEGIN + /* If the table doesn't have the new spid column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''spid'') + /*Add spid column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD spid smallint NULL;'; - */'; + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + /* If the table doesn't have the new wait_resource column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''wait_resource'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD wait_resource nvarchar(MAX) NULL;'; - DECLARE @ProductVersion NVARCHAR(128); - DECLARE @ProductVersionMajor FLOAT; - DECLARE @ProductVersionMinor INT; + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; - SET @ProductVersion = CAST(SERVERPROPERTY('ProductVersion') AS NVARCHAR(128)); + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_1'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_1 varchar(500) NULL;'; - SELECT @ProductVersionMajor = SUBSTRING(@ProductVersion, 1, CHARINDEX('.', @ProductVersion) + 1), - @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2); + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + /* If the table doesn't have the new client option column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''client_option_2'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD client_option_2 varchar(500) NULL;'; - IF @ProductVersionMajor < 11.0 + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new lock mode column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''lock_mode'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD lock_mode nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /* If the table doesn't have the new status column, add it. See Github #3101. */ + SET @StringToExecute = + N'IF NOT EXISTS (SELECT 1/0 FROM ' + + @OutputDatabaseName + + N'.sys.all_columns AS o WHERE o.object_id = (OBJECT_ID(''' + + @ObjectFullName + + N''')) AND o.name = N''status'') + /*Add wait_resource column*/ + ALTER TABLE ' + + @ObjectFullName + + N' ADD status nvarchar(256) NULL;'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + 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 */ BEGIN - RAISERROR( - 'sp_BlitzLock will throw a bunch of angry errors on versions of SQL Server earlier than 2012.', - 0, - 1) WITH NOWAIT; - RETURN; + SELECT + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableName + + N' ( + ServerName nvarchar(256), + deadlock_type nvarchar(256), + event_date datetime, + database_name nvarchar(256), + spid smallint, + deadlock_group nvarchar(256), + query xml, + object_names xml, + isolation_level nvarchar(256), + owner_mode nvarchar(256), + waiter_mode nvarchar(256), + lock_mode nvarchar(256), + transaction_count bigint, + client_option_1 varchar(500), + client_option_2 varchar(500), + login_name nvarchar(256), + host_name nvarchar(256), + client_app nvarchar(1024), + wait_time bigint, + wait_resource nvarchar(max), + priority smallint, + log_used bigint, + last_tran_started datetime, + last_batch_started datetime, + last_batch_completed datetime, + transaction_name nvarchar(256), + status nvarchar(256), + owner_waiter_type nvarchar(256), + owner_activity nvarchar(256), + owner_waiter_activity nvarchar(256), + owner_merging nvarchar(256), + owner_spilling nvarchar(256), + owner_waiting_to_close nvarchar(256), + waiter_waiter_type nvarchar(256), + waiter_owner_activity nvarchar(256), + waiter_waiter_activity nvarchar(256), + waiter_merging nvarchar(256), + waiter_spilling nvarchar(256), + waiter_waiting_to_close nvarchar(256), + deadlock_graph xml + )'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*table created.*/ + SELECT + @StringToExecute = + N'SELECT @r = o.name FROM ' + + @OutputDatabaseName + + N'.sys.objects AS o + WHERE o.type_desc = N''USER_TABLE'' + AND o.name = N''BlitzLockFindings''', + @StringToExecuteParams = + N'@r sysname OUTPUT'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute, + @StringToExecuteParams, + @r OUTPUT; + + IF (@r IS NULL) /*if table does not exist*/ + BEGIN + SELECT + @OutputTableFindings = + QUOTENAME(N'BlitzLockFindings'), + @StringToExecute = + N'USE ' + + @OutputDatabaseName + + N'; + CREATE TABLE ' + + @OutputSchemaName + + N'.' + + @OutputTableFindings + + N' ( + ServerName nvarchar(256), + check_id INT, + database_name nvarchar(256), + object_name nvarchar(1000), + finding_group nvarchar(100), + finding nvarchar(4000) + );'; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; END; - IF @Top IS NULL - SET @Top = 2147483647; - - IF @StartDate IS NULL - SET @StartDate = '19000101'; - - IF @EndDate IS NULL - SET @EndDate = '99991231'; - - - IF OBJECT_ID('tempdb..#deadlock_data') IS NOT NULL - DROP TABLE #deadlock_data; - - IF OBJECT_ID('tempdb..#deadlock_process') IS NOT NULL - DROP TABLE #deadlock_process; - - IF OBJECT_ID('tempdb..#deadlock_stack') IS NOT NULL - DROP TABLE #deadlock_stack; - - IF OBJECT_ID('tempdb..#deadlock_resource') IS NOT NULL - DROP TABLE #deadlock_resource; - - IF OBJECT_ID('tempdb..#deadlock_owner_waiter') IS NOT NULL - DROP TABLE #deadlock_owner_waiter; - - IF OBJECT_ID('tempdb..#deadlock_findings') IS NOT NULL - DROP TABLE #deadlock_findings; - - CREATE TABLE #deadlock_findings - ( - id INT IDENTITY(1, 1) PRIMARY KEY CLUSTERED, - check_id INT NOT NULL, - database_name NVARCHAR(256), - object_name NVARCHAR(1000), - finding_group NVARCHAR(100), - finding NVARCHAR(4000) - ); - - - /*Grab the initial set of XML to parse*/ - WITH xml - AS ( SELECT CONVERT(XML, event_data) AS deadlock_xml - FROM sys.fn_xe_file_target_read_file(@EventSessionPath, NULL, NULL, NULL) ) - SELECT TOP ( @Top ) xml.deadlock_xml - INTO #deadlock_data - FROM xml - WHERE xml.deadlock_xml.value('(/event/@name)[1]', 'VARCHAR(256)') = 'xml_deadlock_report' - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') >= @StartDate - AND xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') < @EndDate - ORDER BY xml.deadlock_xml.value('(/event/@timestamp)[1]', 'datetime') - OPTION ( RECOMPILE ); - - - - /*Parse process and input buffer XML*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.value('@id', 'NVARCHAR(256)') AS id, - ca.dp.value('@currentdb', 'BIGINT') AS database_id, - ca.dp.value('@logused', 'BIGINT') AS log_used, - ca.dp.value('@waitresource', 'NVARCHAR(256)') AS wait_resource, - ca.dp.value('@waittime', 'BIGINT') AS wait_time, - ca.dp.value('@transactionname', 'NVARCHAR(256)') AS transaction_name, - ca.dp.value('@lasttranstarted', 'DATETIME2(7)') AS last_tran_started, - ca.dp.value('@lastbatchstarted', 'DATETIME2(7)') AS last_batch_started, - ca.dp.value('@lastbatchcompleted', 'DATETIME2(7)') AS last_batch_completed, - ca.dp.value('@lockMode', 'NVARCHAR(256)') AS lock_mode, - ca.dp.value('@trancount', 'BIGINT') AS transaction_count, - ca.dp.value('@clientapp', 'NVARCHAR(256)') AS client_app, - ca.dp.value('@hostname', 'NVARCHAR(256)') AS host_name, - ca.dp.value('@loginname', 'NVARCHAR(256)') AS login_name, - ca.dp.value('@isolationlevel', 'NVARCHAR(256)') AS isolation_level, - ca2.ib.query('.') AS input_buffer, - ca.dp.query('.') AS process_xml - INTO #deadlock_process - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process/inputbuf') AS ca2(ib) - WHERE (ca.dp.value('@currentdb', 'BIGINT') = DB_ID(@DatabaseName) OR @DatabaseName IS NULL) - AND (ca.dp.value('@clientapp', 'NVARCHAR(256)') = @AppName OR @AppName IS NULL) - AND (ca.dp.value('@hostname', 'NVARCHAR(256)') = @HostName OR @HostName IS NULL) - AND (ca.dp.value('@loginname', 'NVARCHAR(256)') = @LoginName OR @LoginName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Parse execution stack XML*/ - SELECT dp.id, - dp.event_date, - ca.dp.value('@procname', 'NVARCHAR(1000)') AS proc_name, - ca.dp.value('@sqlhandle', 'NVARCHAR(128)') AS sql_handle - INTO #deadlock_stack - FROM #deadlock_process AS dp + /*create synonym for deadlockfindings.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadlockFindings' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadlockFindings, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadlockFindings; + END; + + RAISERROR('Creating synonym DeadlockFindings', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadlockFindings FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableFindings; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + /*create synonym for deadlock table.*/ + IF EXISTS + ( + SELECT + 1/0 + FROM sys.objects AS o + WHERE o.name = N'DeadLockTbl' + AND o.type_desc = N'SYNONYM' + ) + BEGIN + RAISERROR('Found synonym DeadLockTbl, dropping', 0, 1) WITH NOWAIT; + DROP SYNONYM dbo.DeadLockTbl; + END; + + RAISERROR('Creating synonym DeadLockTbl', 0, 1) WITH NOWAIT; + SET @StringToExecute = + N'CREATE SYNONYM dbo.DeadLockTbl FOR ' + + @OutputDatabaseName + + N'.' + + @OutputSchemaName + + N'.' + + @OutputTableName; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + END; + END; + + /* WITH ROWCOUNT doesn't work on Amazon RDS - see: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2037 */ + IF @RDS = 0 + BEGIN; + BEGIN TRY; + RAISERROR('@RDS = 0, updating #t with high row and page counts', 0, 1) WITH NOWAIT; + UPDATE STATISTICS + #t + WITH + ROWCOUNT = 9223372036854775807, + PAGECOUNT = 9223372036854775807; + END TRY + BEGIN CATCH; + /* Misleading error returned, if run without permissions to update statistics the error returned is "Cannot find object". + Catching specific error, and returning message with better info. If any other error is returned, then throw as normal */ + IF (ERROR_NUMBER() = 1088) + BEGIN; + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Cannot run UPDATE STATISTICS on a #temp table without db_owner or sysadmin permissions', 0, 1) WITH NOWAIT; + END; + ELSE + BEGIN; + THROW; + END; + END CATCH; + END; + + IF @DeadlockType IS NOT NULL + BEGIN + SELECT + @DeadlockType = + CASE + WHEN LOWER(@DeadlockType) LIKE 'regular%' + THEN N'Regular Deadlock' + WHEN LOWER(@DeadlockType) LIKE N'parallel%' + THEN N'Parallel Deadlock' + ELSE NULL + END; + END; + + /*If @TargetSessionType, we need to figure out if it's ring buffer or event file*/ + /*Azure has differently named views, so we need to separate. Thanks, Azure.*/ + + IF + ( + @Azure = 0 + AND @TargetSessionType IS NULL + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for non-Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_sessions AS s + JOIN sys.dm_xe_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for non-Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; + + IF + ( + @Azure = 1 + AND @TargetSessionType IS NULL + AND LOWER(@TargetSessionType) <> N'table' + ) + BEGIN + RAISERROR('@TargetSessionType is NULL, assigning for Azure instance', 0, 1) WITH NOWAIT; + + SELECT TOP (1) + @TargetSessionType = t.target_name + FROM sys.dm_xe_database_sessions AS s + JOIN sys.dm_xe_database_session_targets AS t + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name IN (N'event_file', N'ring_buffer') + ORDER BY t.target_name + OPTION(RECOMPILE); + + RAISERROR('@TargetSessionType assigned as %s for Azure', 0, 1, @TargetSessionType) WITH NOWAIT; + END; + + + /*The system health stuff gets handled different from user extended events.*/ + /*These next sections deal with user events, dependent on target.*/ + + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for non-Azure at %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_session_targets AS t + JOIN sys.dm_xe_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('@TargetSessionType is ring_buffer, inserting XML for Azure at %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(t.target_data AS xml) + FROM sys.dm_xe_database_session_targets AS t + JOIN sys.dm_xe_database_sessions AS s + ON s.address = t.event_session_address + WHERE s.name = @EventSessionName + AND t.target_name = N'ring_buffer' + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + END; + + + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + IF @Azure = 0 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for non-Azure', 0, 1) WITH NOWAIT; + + SELECT + @SessionId = t.event_session_id, + @TargetSessionId = t.target_id + FROM sys.server_event_session_targets AS t + JOIN sys.server_event_sessions AS s + ON s.event_session_id = t.event_session_id + WHERE t.name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; + + IF @Azure = 1 + BEGIN + RAISERROR('@TargetSessionType is event_file, assigning XML for Azure', 0, 1) WITH NOWAIT; + SELECT + @SessionId = + t.event_session_address, + @TargetSessionId = + t.target_name + FROM sys.dm_xe_database_session_targets t + JOIN sys.dm_xe_database_sessions s + ON s.address = t.event_session_address + WHERE t.target_name = @TargetSessionType + AND s.name = @EventSessionName + OPTION(RECOMPILE); + + /*We get the file name automatically, here*/ + RAISERROR('Assigning @FileName...', 0, 1) WITH NOWAIT; + SELECT + @FileName = + CASE + WHEN f.file_name LIKE N'%.xel' + THEN REPLACE(f.file_name, N'.xel', N'*.xel') + ELSE f.file_name + N'*.xel' + END + FROM + ( + SELECT + file_name = + CONVERT(nvarchar(4000), f.value) + FROM sys.server_event_session_fields AS f + WHERE f.event_session_id = @SessionId + AND f.object_id = @TargetSessionId + AND f.name = N'filename' + ) AS f + OPTION(RECOMPILE); + END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Reading for event_file %s', 0, 1, @FileName) WITH NOWAIT; + + INSERT + #x WITH(TABLOCKX) + ( + x + ) + SELECT + x = TRY_CAST(f.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(@FileName, NULL, NULL, NULL) AS f + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*The XML is parsed differently if it comes from the event file or ring buffer*/ + + /*If ring buffers*/ + IF + ( + @TargetSessionType LIKE N'ring%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for ring buffer data', 0, 1) WITH NOWAIT; + + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query(N'.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.nodes('/RingBufferTarget/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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*If event file*/ + IF + ( + @TargetSessionType LIKE N'event_file%' + AND @EventSessionName NOT LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to #deadlock_data for event file data', 0, 1) WITH NOWAIT; + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + INSERT + #deadlock_data WITH(TABLOCKX) + ( + deadlock_xml + ) + SELECT + deadlock_xml = + e.x.query('.') + FROM #x AS x + LEFT JOIN #t AS t + ON 1 = 1 + CROSS APPLY x.x.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 + ) + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*This section deals with event file*/ + IF + ( + @TargetSessionType LIKE N'event%' + AND @EventSessionName LIKE N'system_health%' + ) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Grab the initial set of xml to parse at %s', 0, 1, @d) WITH NOWAIT; + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + SELECT + xml.deadlock_xml + INTO #xml + FROM + ( + SELECT + deadlock_xml = + TRY_CAST(fx.event_data AS xml) + FROM sys.fn_xe_file_target_read_file(N'system_health*.xel', NULL, NULL, NULL) AS fx + LEFT JOIN #t AS t + ON 1 = 1 + WHERE fx.object_name = N'xml_deadlock_report' + ) AS xml + CROSS APPLY xml.deadlock_xml.nodes('/event') AS e(x) + WHERE 1 = 1 + AND e.x.exist('@timestamp[. >= sql:variable("@StartDate") and .< sql:variable("@EndDate")]') = 1 + OPTION(RECOMPILE); + + INSERT + #deadlock_data WITH(TABLOCKX) + SELECT + deadlock_xml = + xml.deadlock_xml + FROM #xml AS xml + LEFT JOIN #t AS t + ON 1 = 1 + WHERE xml.deadlock_xml IS NOT NULL + OPTION(RECOMPILE); + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /* If table target */ + 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; + + /* + 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 ' + + 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); + '; + + 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; + + SELECT + d1.deadlock_xml, + event_date = d1.deadlock_xml.value('(event/@timestamp)[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('/event/data/value/deadlock') + INTO #dd + FROM #deadlock_data AS d1 + LEFT JOIN #t AS t + ON 1 = 1 + WHERE @xe = 1 + OR LOWER(@TargetSessionType) <> N'table' + + 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); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Final Parse process and input buffer xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT + q.event_date, + q.victim_id, + is_parallel = + CONVERT(bit, q.is_parallel), + q.deadlock_graph, + q.id, + q.spid, + q.database_id, + database_name = + ISNULL + ( + DB_NAME(q.database_id), + N'UNKNOWN' + ), + q.current_database_name, + q.priority, + q.log_used, + q.wait_resource, + q.wait_time, + q.transaction_name, + q.last_tran_started, + q.last_batch_started, + q.last_batch_completed, + q.lock_mode, + q.status, + q.transaction_count, + q.client_app, + q.host_name, + q.login_name, + q.isolation_level, + client_option_1 = + SUBSTRING + ( + CASE WHEN q.clientoption1 & 1 = 1 THEN ', DISABLE_DEF_CNST_CHECK' ELSE '' END + + CASE WHEN q.clientoption1 & 2 = 2 THEN ', IMPLICIT_TRANSACTIONS' ELSE '' END + + CASE WHEN q.clientoption1 & 4 = 4 THEN ', CURSOR_CLOSE_ON_COMMIT' ELSE '' END + + CASE WHEN q.clientoption1 & 8 = 8 THEN ', ANSI_WARNINGS' ELSE '' END + + CASE WHEN q.clientoption1 & 16 = 16 THEN ', ANSI_PADDING' ELSE '' END + + CASE WHEN q.clientoption1 & 32 = 32 THEN ', ANSI_NULLS' ELSE '' END + + CASE WHEN q.clientoption1 & 64 = 64 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 128 = 128 THEN ', ARITHIGNORE' ELSE '' END + + CASE WHEN q.clientoption1 & 256 = 256 THEN ', QUOTED_IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption1 & 512 = 512 THEN ', NOCOUNT' ELSE '' END + + CASE WHEN q.clientoption1 & 1024 = 1024 THEN ', ANSI_NULL_DFLT_ON' ELSE '' END + + CASE WHEN q.clientoption1 & 2048 = 2048 THEN ', ANSI_NULL_DFLT_OFF' ELSE '' END + + CASE WHEN q.clientoption1 & 4096 = 4096 THEN ', CONCAT_NULL_YIELDS_NULL' ELSE '' END + + CASE WHEN q.clientoption1 & 8192 = 8192 THEN ', NUMERIC_ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption1 & 16384 = 16384 THEN ', XACT_ABORT' ELSE '' END, + 3, + 500 + ), + client_option_2 = + SUBSTRING + ( + CASE WHEN q.clientoption2 & 1024 = 1024 THEN ', DB CHAINING' ELSE '' END + + CASE WHEN q.clientoption2 & 2048 = 2048 THEN ', NUMERIC ROUNDABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 4096 = 4096 THEN ', ARITHABORT' ELSE '' END + + CASE WHEN q.clientoption2 & 8192 = 8192 THEN ', ANSI PADDING' ELSE '' END + + CASE WHEN q.clientoption2 & 16384 = 16384 THEN ', ANSI NULL DEFAULT' ELSE '' END + + CASE WHEN q.clientoption2 & 65536 = 65536 THEN ', CONCAT NULL YIELDS NULL' ELSE '' END + + CASE WHEN q.clientoption2 & 131072 = 131072 THEN ', RECURSIVE TRIGGERS' ELSE '' END + + CASE WHEN q.clientoption2 & 1048576 = 1048576 THEN ', DEFAULT TO LOCAL CURSOR' ELSE '' END + + CASE WHEN q.clientoption2 & 8388608 = 8388608 THEN ', QUOTED IDENTIFIER' ELSE '' END + + CASE WHEN q.clientoption2 & 16777216 = 16777216 THEN ', AUTO CREATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 33554432 = 33554432 THEN ', CURSOR CLOSE ON COMMIT' ELSE '' END + + CASE WHEN q.clientoption2 & 67108864 = 67108864 THEN ', ANSI NULLS' ELSE '' END + + CASE WHEN q.clientoption2 & 268435456 = 268435456 THEN ', ANSI WARNINGS' ELSE '' END + + CASE WHEN q.clientoption2 & 536870912 = 536870912 THEN ', FULL TEXT ENABLED' ELSE '' END + + CASE WHEN q.clientoption2 & 1073741824 = 1073741824 THEN ', AUTO UPDATE STATISTICS' ELSE '' END + + CASE WHEN q.clientoption2 & 1469283328 = 1469283328 THEN ', ALL SETTABLE OPTIONS' ELSE '' END, + 3, + 500 + ), + q.process_xml + INTO #deadlock_process + FROM + ( + SELECT + dd.deadlock_xml, + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dd.event_date + ), + dd.victim_id, + is_parallel = + CONVERT(tinyint, dd.is_parallel) + + CONVERT(tinyint, dd.is_parallel_batch), + dd.deadlock_graph, + id = ca.dp.value('@id', 'nvarchar(256)'), + spid = ca.dp.value('@spid', 'smallint'), + database_id = ca.dp.value('@currentdb', 'bigint'), + current_database_name = ca.dp.value('@currentdbname', 'nvarchar(256)'), + priority = ca.dp.value('@priority', 'smallint'), + log_used = ca.dp.value('@logused', 'bigint'), + wait_resource = ca.dp.value('@waitresource', 'nvarchar(256)'), + wait_time = ca.dp.value('@waittime', 'bigint'), + transaction_name = ca.dp.value('@transactionname', 'nvarchar(256)'), + last_tran_started = ca.dp.value('@lasttranstarted', 'datetime'), + last_batch_started = ca.dp.value('@lastbatchstarted', 'datetime'), + last_batch_completed = ca.dp.value('@lastbatchcompleted', 'datetime'), + lock_mode = ca.dp.value('@lockMode', 'nvarchar(256)'), + status = ca.dp.value('@status', 'nvarchar(256)'), + transaction_count = ca.dp.value('@trancount', 'bigint'), + client_app = ca.dp.value('@clientapp', 'nvarchar(1024)'), + host_name = ca.dp.value('@hostname', 'nvarchar(256)'), + login_name = ca.dp.value('@loginname', 'nvarchar(256)'), + isolation_level = ca.dp.value('@isolationlevel', 'nvarchar(256)'), + clientoption1 = ca.dp.value('@clientoption1', 'bigint'), + clientoption2 = ca.dp.value('@clientoption2', 'bigint'), + process_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #dd AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) + WHERE (ca.dp.exist('@currentdb[. = sql:variable("@DatabaseId")]') = 1 OR @DatabaseName IS NULL) + AND (ca.dp.exist('@clientapp[. = sql:variable("@AppName")]') = 1 OR @AppName IS NULL) + AND (ca.dp.exist('@hostname[. = sql:variable("@HostName")]') = 1 OR @HostName IS NULL) + AND (ca.dp.exist('@loginname[. = sql:variable("@LoginName")]') = 1 OR @LoginName IS NULL) + ) AS q + LEFT JOIN #t AS t + ON 1 = 1 + OPTION(RECOMPILE); + + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse execution stack xml*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse execution stack xml %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + dp.id, + dp.event_date, + proc_name = ca.dp.value('@procname', 'nvarchar(1024)'), + sql_handle = ca.dp.value('@sqlhandle', 'nvarchar(131)') + INTO #deadlock_stack + FROM #deadlock_process AS dp CROSS APPLY dp.process_xml.nodes('//executionStack/frame') AS ca(dp) - WHERE (ca.dp.value('@procname', 'NVARCHAR(256)') = @StoredProcName OR @StoredProcName IS NULL) - OPTION ( RECOMPILE ); - - - - /*Grab the full resource list*/ - SELECT dd.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, - dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, - ca.dp.query('.') AS resource_xml - INTO #deadlock_resource - FROM #deadlock_data AS dd - CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) - OPTION ( RECOMPILE ); - - - /*This parses object locks*/ - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(1000)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - INTO #deadlock_owner_waiter - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) + WHERE (ca.dp.exist('@procname[. = sql:variable("@StoredProcName")]') = 1 OR @StoredProcName IS NULL) + AND ca.dp.exist('@sqlhandle[ .= "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"]') = 0 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Grab the full resource list*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + + RAISERROR('Grab the full resource list %s', 0, 1, @d) WITH NOWAIT; + + SELECT + event_date = + DATEADD + ( + MINUTE, + DATEDIFF + ( + MINUTE, + GETUTCDATE(), + SYSDATETIME() + ), + dr.event_date + ), + dr.victim_id, + dr.resource_xml + INTO + #deadlock_resource + FROM + ( + SELECT + event_date = dd.deadlock_xml.value('(event/@timestamp)[1]', 'datetime2'), + victim_id = dd.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'nvarchar(256)'), + resource_xml = ISNULL(ca.dp.query(N'.'), N'') + FROM #deadlock_data AS dd + CROSS APPLY dd.deadlock_xml.nodes('//deadlock/resource-list') AS ca(dp) + ) AS dr + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse object locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse object locks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + object_name = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + ca.object_name COLLATE Latin1_General_BIN2, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?'), + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = CAST(N'OBJECT' AS nvarchar(100)) + INTO #deadlock_owner_waiter + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - WHERE (ca.dr.value('@objectname', 'NVARCHAR(1000)') = @ObjectName OR @ObjectName IS NULL) - OPTION ( RECOMPILE ); - - - - /*This parses page locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse page locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse page locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'PAGE' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses key locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse key locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse key locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'KEY' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - - /*This parses rid locks*/ - INSERT #deadlock_owner_waiter - SELECT dr.event_date, - ca.dr.value('@dbid', 'BIGINT') AS database_id, - ca.dr.value('@objectname', 'NVARCHAR(256)') AS object_name, - ca.dr.value('@mode', 'NVARCHAR(256)') AS lock_mode, - w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, - w.l.value('@mode', 'NVARCHAR(256)') AS waiter_mode, - o.l.value('@id', 'NVARCHAR(256)') AS owner_id, - o.l.value('@mode', 'NVARCHAR(256)') AS owner_mode - FROM #deadlock_resource AS dr - CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse RID locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse RID locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'RID' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) - OPTION ( RECOMPILE ); - - /*Get rid of nonsense*/ - DELETE dow - FROM #deadlock_owner_waiter AS dow - WHERE dow.owner_id = dow.waiter_id; - - /*Add some nonsense*/ - ALTER TABLE #deadlock_process - ADD waiter_mode NVARCHAR(256), - owner_mode NVARCHAR(256), - is_victim AS CONVERT(BIT, CASE WHEN id = victim_id THEN 1 ELSE 0 END); - - /*Update some nonsense*/ - UPDATE dp - SET dp.owner_mode = dow.owner_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 0; - - UPDATE dp - SET dp.waiter_mode = dow.waiter_mode - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.victim_id = dow.waiter_id - AND dp.event_date = dow.event_date - WHERE dp.is_victim = 1; - - - /*Begin checks based on parsed values*/ - - /*Check 1 is deadlocks by database*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 1 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Total database locks' AS finding_group, - 'This database had ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) - + ' deadlocks.' - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - /*Check 2 is deadlocks by object*/ - - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 2 AS check_id, - DB_NAME(dow.database_id) AS database_name, - dow.object_name AS object_name, - 'Total object deadlocks' AS finding_group, - 'This object was involved in ' - + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.object_name)) - + ' deadlock(s).' - FROM #deadlock_owner_waiter AS dow - GROUP BY DB_NAME(dow.database_id), dow.object_name - OPTION ( RECOMPILE ); - - - /*Check 3 looks for Serializable locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 3 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Serializable locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of serializable deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'serializable%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 4 looks for Repeatable Read locking*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 4 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Repeatable Read locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(*)) + - ' instances of repeatable read deadlocks.' - AS finding - FROM #deadlock_process AS dp - WHERE dp.isolation_level LIKE 'repeatable read%' - GROUP BY DB_NAME(dp.database_id) - OPTION ( RECOMPILE ); - - - /*Check 5 breaks down app, host, and login information*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 5 AS check_id, - DB_NAME(dp.database_id) AS database_name, - '-' AS object_name, - 'Login, App, and Host locking' AS finding_group, - 'This database has had ' + - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.event_date)) + - ' instances of deadlocks involving the login ' + - ISNULL(dp.login_name, 'UNKNOWN') + - ' from the application ' + - ISNULL(dp.client_app, 'UNKNOWN') + - ' on host ' + - ISNULL(dp.host_name, 'UNKNOWN') - AS finding - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id), dp.login_name, dp.client_app, dp.host_name - OPTION ( RECOMPILE ); - - - /*Check 6 breaks down the types of locks (object, page, key, etc.)*/ - WITH lock_types AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - dow.object_name, - SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) -1) AS lock, - CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dp.id)) AS lock_count - FROM #deadlock_process AS dp - JOIN #deadlock_owner_waiter AS dow - ON dp.id = dow.owner_id - AND dp.event_date = dow.event_date - GROUP BY DB_NAME(dp.database_id), SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) - 1), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 6 AS check_id, - lt.database_name, - lt.object_name, - 'Types of locks by object' AS finding_group, - 'This object has had ' + - STUFF((SELECT DISTINCT N', ' + lt2.lock_count + ' ' + 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'') - + ' locks' - FROM lock_types AS lt - OPTION ( RECOMPILE ); - - - /*Check 7 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name, - '''' + STUFF((SELECT DISTINCT N',' + ds2.sql_handle - FROM #deadlock_stack AS ds2 - WHERE ds2.id = ds.id - AND ds2.event_date = ds.event_date - FOR XML PATH(N''), TYPE).value(N'.[1]', N'NVARCHAR(MAX)'), 1, 1, N'') + '''' AS sql_handle_csv - FROM #deadlock_stack AS ds - GROUP BY PARSENAME(ds.proc_name, 3), - PARSENAME(ds.proc_name, 2), - PARSENAME(ds.proc_name, 1), - ds.id, - ds.proc_name, - ds.event_date - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzCache ' + - CASE WHEN ds.proc_name = 'adhoc' - THEN ' @OnlySqlHandles = ' + sql_handle_csv - ELSE '@StoredProcName = ' + - QUOTENAME(ds.proc_only_name, '''') - END + - ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - OPTION ( RECOMPILE ); - - IF @ProductVersionMajor >= 13 - BEGIN - - WITH deadlock_stack AS ( - SELECT DISTINCT - ds.id, - ds.sql_handle, - ds.proc_name, - ds.event_date, - PARSENAME(ds.proc_name, 3) AS database_name, - PARSENAME(ds.proc_name, 2) AS schema_name, - PARSENAME(ds.proc_name, 1) AS proc_only_name - FROM #deadlock_stack AS ds - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT DISTINCT 7 AS check_id, - DB_NAME(dow.database_id) AS database_name, - ds.proc_name AS object_name, - 'More Info - Query' AS finding_group, - 'EXEC sp_BlitzQueryStore ' - + '@DatabaseName = ' - + QUOTENAME(ds.database_name, '''') - + ', ' - + '@StoredProcName = ' - + QUOTENAME(ds.proc_only_name, '''') - + ';' AS finding - FROM deadlock_stack AS ds - JOIN #deadlock_owner_waiter AS dow - ON dow.owner_id = ds.id - AND dow.event_date = ds.event_date - WHERE ds.proc_name <> 'adhoc' - OPTION ( RECOMPILE ); - END; - - - /*Check 8 gives you stored proc deadlock counts*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 8 AS check_id, - DB_NAME(dp.database_id) AS database_name, - ds.proc_name, - 'Stored Procedure Deadlocks', - 'The stored procedure ' - + PARSENAME(ds.proc_name, 2) - + '.' - + PARSENAME(ds.proc_name, 1) - + ' has been involved in ' - + CONVERT(NVARCHAR(10), COUNT_BIG(DISTINCT ds.id)) - + ' deadlocks.' - FROM #deadlock_stack AS ds - JOIN #deadlock_process AS dp - ON dp.id = ds.id - AND ds.event_date = dp.event_date - WHERE ds.proc_name <> 'adhoc' - GROUP BY DB_NAME(dp.database_id), ds.proc_name - OPTION(RECOMPILE); - - - /*Check 9 gives you more info queries for sp_BlitzIndex */ - WITH bi AS ( - SELECT DISTINCT - dow.object_name, - PARSENAME(dow.object_name, 3) AS database_name, - PARSENAME(dow.object_name, 2) AS schema_name, - PARSENAME(dow.object_name, 1) AS table_name - FROM #deadlock_owner_waiter AS dow - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 9 AS check_id, - bi.database_name, - bi.schema_name + '.' + bi.table_name, - 'More Info - Table' AS finding_group, - 'EXEC sp_BlitzIndex ' + - '@DatabaseName = ' + QUOTENAME(bi.database_name, '''') + - ', @SchemaName = ' + QUOTENAME(bi.schema_name, '''') + - ', @TableName = ' + QUOTENAME(bi.table_name, '''') + - ';' AS finding - FROM bi - OPTION ( RECOMPILE ); - - /*Check 10 gets total deadlock wait time per object*/ - WITH chopsuey AS ( - SELECT DISTINCT - PARSENAME(dow.object_name, 3) AS database_name, - dow.object_name, - CONVERT(VARCHAR(10), (SUM(DISTINCT dp.wait_time) / 1000) / 86400) AS wait_days, - CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT dp.wait_time) / 1000), 0), 108) AS wait_time_hms - FROM #deadlock_owner_waiter AS dow - JOIN #deadlock_process AS dp - ON (dp.id = dow.owner_id OR dp.victim_id = dow.waiter_id) - AND dp.event_date = dow.event_date - GROUP BY PARSENAME(dow.object_name, 3), dow.object_name - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 10 AS check_id, - cs.database_name, - cs.object_name, - 'Total object deadlock wait time' AS finding_group, - 'This object has had ' - + CONVERT(VARCHAR(10), cs.wait_days) - + ':' + CONVERT(VARCHAR(20), cs.wait_time_hms, 108) - + ' [d/h/m/s] of deadlock wait time.' AS finding - FROM chopsuey AS cs - WHERE cs.object_name IS NOT NULL - OPTION ( RECOMPILE ); - - /*Check 11 gets total deadlock wait time per database*/ - WITH wait_time AS ( - SELECT DB_NAME(dp.database_id) AS database_name, - SUM(CONVERT(BIGINT, dp.wait_time)) AS total_wait_time_ms - FROM #deadlock_process AS dp - GROUP BY DB_NAME(dp.database_id) - ) - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - SELECT 11 AS check_id, - wt.database_name, - '-' AS object_name, - 'Total database deadlock wait time' AS finding_group, - 'This database has had ' - + CONVERT(VARCHAR(10), (SUM(DISTINCT wt.total_wait_time_ms) / 1000) / 86400) - + ':' + CONVERT(VARCHAR(20), DATEADD(SECOND, (SUM(DISTINCT wt.total_wait_time_ms) / 1000), 0), 108) - + ' [d/h/m/s] of deadlock wait time.' - FROM wait_time AS wt - GROUP BY wt.database_name - OPTION ( RECOMPILE ); - - - /*Thank you goodnight*/ - INSERT #deadlock_findings ( check_id, database_name, object_name, finding_group, finding ) - VALUES ( -1, - N'sp_BlitzLock ' + CAST(CONVERT(DATETIME, @VersionDate, 102) AS VARCHAR(100)), - N'SQL Server First Responder Kit', - N'http://FirstResponderKit.org/', - N'To get help or add your own contributions, join us at http://FirstResponderKit.org.'); - - - - - /*Results*/ - WITH deadlocks - AS ( SELECT dp.event_date, - dp.id, - dp.victim_id, - dp.database_id, - dp.log_used, - dp.wait_resource, - CONVERT( - XML, - STUFF(( SELECT DISTINCT NCHAR(10) - + N' ' - + ISNULL(c.object_name, N'') - + N' ' AS object_name - FROM #deadlock_owner_waiter AS c - WHERE (dp.id = c.owner_id - OR dp.victim_id = c.waiter_id) - AND dp.event_date = c.event_date - FOR XML PATH(N''), TYPE ).value(N'.[1]', N'NVARCHAR(4000)'), - 1, 1, N'')) AS object_names, - dp.wait_time, - dp.transaction_name, - dp.last_tran_started, - dp.last_batch_started, - dp.last_batch_completed, - dp.lock_mode, - dp.transaction_count, - dp.client_app, - dp.host_name, - dp.login_name, - dp.isolation_level, - dp.process_xml.value('(//process/inputbuf/text())[1]', 'NVARCHAR(MAX)') AS inputbuf, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date ) AS dn, - DENSE_RANK() OVER ( ORDER BY dp.event_date ) AS en, - ROW_NUMBER() OVER ( PARTITION BY dp.event_date ORDER BY dp.event_date ) -1 AS qn, - dp.is_victim, - ISNULL(dp.owner_mode, '-') AS owner_mode, - ISNULL(dp.waiter_mode, '-') AS waiter_mode - FROM #deadlock_process AS dp ) - SELECT d.event_date, - DB_NAME(d.database_id) AS database_name, - 'Deadlock #' - + CONVERT(NVARCHAR(10), d.en) - + ', Query #' - + CASE WHEN d.qn = 0 THEN N'1' ELSE CONVERT(NVARCHAR(10), d.qn) END - + CASE WHEN d.is_victim = 1 THEN ' - VICTIM' ELSE '' END - AS deadlock_group, - CONVERT(XML, N'' + d.inputbuf + N'') AS query, - d.object_names, - d.isolation_level, - d.owner_mode, - d.waiter_mode, - d.transaction_count, - d.login_name, - d.host_name, - d.client_app, - d.wait_time, - d.log_used, - d.last_tran_started, - d.last_batch_started, - d.last_batch_completed, - d.transaction_name - FROM deadlocks AS d - WHERE d.dn = 1 - ORDER BY d.event_date, is_victim DESC; - - - - SELECT df.check_id, df.database_name, df.object_name, df.finding_group, df.finding - FROM #deadlock_findings AS df - ORDER BY df.check_id - OPTION ( RECOMPILE ); + OPTION(RECOMPILE); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; - IF @Debug = 1 + /*Parse row group locks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse row group locks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_owner_waiter WITH(TABLOCKX) + SELECT DISTINCT + ca.event_date, + ca.database_id, + database_name = + ISNULL + ( + DB_NAME(ca.database_id), + N'UNKNOWN' + ), + ca.object_name, + ca.lock_mode, + ca.index_name, + ca.associatedObjectId, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + waiter_mode = w.l.value('@mode', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)'), + owner_mode = o.l.value('@mode', 'nvarchar(256)'), + lock_type = N'ROWGROUP' + FROM + ( + SELECT + dr.event_date, + database_id = ca.dr.value('@dbid', 'bigint'), + object_name = ca.dr.value('@objectname', 'nvarchar(1024)'), + lock_mode = ca.dr.value('@mode', 'nvarchar(256)'), + index_name = ca.dr.value('@indexname', 'nvarchar(256)'), + associatedObjectId = ca.dr.value('@associatedObjectId', 'bigint'), + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/rowgrouplock') AS ca(dr) + WHERE (ca.dr.exist('@objectname[. = sql:variable("@ObjectName")]') = 1 OR @ObjectName IS NULL) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Fixing heaps in #deadlock_owner_waiter %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + d + SET + d.index_name = + d.object_name + N'.HEAP' + FROM #deadlock_owner_waiter AS d + WHERE d.lock_type IN + ( + N'HEAP', + N'RID' + ) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Parse parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Parse parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + SELECT DISTINCT + ca.id, + ca.event_date, + ca.wait_type, + ca.node_id, + ca.waiter_type, + ca.owner_activity, + ca.waiter_activity, + ca.merging, + ca.spilling, + ca.waiting_to_close, + waiter_id = w.l.value('@id', 'nvarchar(256)'), + owner_id = o.l.value('@id', 'nvarchar(256)') + INTO #deadlock_resource_parallel + FROM + ( + SELECT + dr.event_date, + id = ca.dr.value('@id', 'nvarchar(256)'), + wait_type = ca.dr.value('@WaitType', 'nvarchar(256)'), + node_id = ca.dr.value('@nodeId', 'bigint'), + /* These columns are in 2017 CU5+ ONLY */ + waiter_type = ca.dr.value('@waiterType', 'nvarchar(256)'), + owner_activity = ca.dr.value('@ownerActivity', 'nvarchar(256)'), + waiter_activity = ca.dr.value('@waiterActivity', 'nvarchar(256)'), + merging = ca.dr.value('@merging', 'nvarchar(256)'), + spilling = ca.dr.value('@spilling', 'nvarchar(256)'), + waiting_to_close = ca.dr.value('@waitingToClose', 'nvarchar(256)'), + /* These columns are in 2017 CU5+ ONLY */ + dr = ca.dr.query('.') + FROM #deadlock_resource AS dr + CROSS APPLY dr.resource_xml.nodes('//resource-list/exchangeEvent') AS ca(dr) + ) AS ca + CROSS APPLY ca.dr.nodes('//waiter-list/waiter') AS w(l) + CROSS APPLY ca.dr.nodes('//owner-list/owner') AS o(l) + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get rid of parallel noise*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of parallel noise %s', 0, 1, @d) WITH NOWAIT; + + WITH + c AS + ( + SELECT + *, + rn = + ROW_NUMBER() OVER + ( + PARTITION BY + drp.owner_id, + drp.waiter_id + ORDER BY + drp.event_date + ) + FROM #deadlock_resource_parallel AS drp + ) + DELETE + FROM c + WHERE c.rn > 1 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get rid of nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get rid of nonsense %s', 0, 1, @d) WITH NOWAIT; + + DELETE dow + FROM #deadlock_owner_waiter AS dow + WHERE dow.owner_id = dow.waiter_id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Add some nonsense*/ + ALTER TABLE + #deadlock_process + ADD + waiter_mode nvarchar(256), + owner_mode nvarchar(256), + is_victim AS + CONVERT + ( + bit, + CASE + WHEN id = victim_id + THEN 1 + ELSE 0 + END + ) PERSISTED; + + /*Update some nonsense*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 1 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.owner_mode = dow.owner_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.id = dow.owner_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 0 + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Update some nonsense part 2 %s', 0, 1, @d) WITH NOWAIT; + + UPDATE + dp + SET + dp.waiter_mode = dow.waiter_mode + FROM #deadlock_process AS dp + JOIN #deadlock_owner_waiter AS dow + ON dp.victim_id = dow.waiter_id + AND dp.event_date = dow.event_date + WHERE dp.is_victim = 1 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get Agent Job and Step names*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Get Agent Job and Step names %s', 0, 1, @d) WITH NOWAIT; + + SELECT + x.event_date, + x.victim_id, + x.id, + x.database_id, + x.client_app, + x.job_id, + x.step_id, + job_id_guid = + CONVERT + ( + uniqueidentifier, + TRY_CAST + ( + N'' + AS xml + ).value('xs:hexBinary(substring(sql:column("x.job_id"), 0))', 'binary(16)') + ) + INTO #agent_job + FROM + ( + SELECT + dp.event_date, + dp.victim_id, + dp.id, + dp.database_id, + dp.client_app, + job_id = + SUBSTRING + ( + dp.client_app, + CHARINDEX(N'0x', dp.client_app) + LEN(N'0x'), + 32 + ), + step_id = + CASE + WHEN CHARINDEX(N': Step ', dp.client_app) > 0 + AND CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) > 0 + THEN + SUBSTRING + ( + dp.client_app, + CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step '), + CHARINDEX(N')', dp.client_app, CHARINDEX(N': Step ', dp.client_app)) - + (CHARINDEX(N': Step ', dp.client_app) + LEN(N': Step ')) + ) + ELSE dp.client_app + END + FROM #deadlock_process AS dp + WHERE dp.client_app LIKE N'SQLAgent - %' + AND dp.client_app <> N'SQLAgent - Initial Boot Probe' + ) AS x + OPTION(RECOMPILE); + + ALTER TABLE + #agent_job + ADD + job_name nvarchar(256), + step_name nvarchar(256); + + IF + ( + @Azure = 0 + AND @RDS = 0 + ) + BEGIN + SET @StringToExecute = + N' + UPDATE + aj + SET + aj.job_name = j.name, + aj.step_name = s.step_name + FROM msdb.dbo.sysjobs AS j + JOIN msdb.dbo.sysjobsteps AS s + ON j.job_id = s.job_id + JOIN #agent_job AS aj + ON aj.job_id_guid = j.job_id + AND aj.step_id = s.step_id + OPTION(RECOMPILE); + '; + + IF @Debug = 1 BEGIN PRINT @StringToExecute; END; + EXECUTE sys.sp_executesql + @StringToExecute; + + END; + + UPDATE + dp + SET + dp.client_app = + CASE + WHEN dp.client_app LIKE N'SQLAgent - %' + THEN N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name + ELSE dp.client_app + END + FROM #deadlock_process AS dp + JOIN #agent_job AS aj + ON dp.event_date = aj.event_date + AND dp.victim_id = aj.victim_id + AND dp.id = aj.id + OPTION(RECOMPILE); + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Get each and every table of all databases*/ + IF @Azure = 0 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId %s', 0, 1, @d) WITH NOWAIT; + + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + EXECUTE sys.sp_MSforeachdb + N' + SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + USE [?]; + + IF EXISTS + ( + SELECT + 1/0 + FROM #deadlock_process AS dp + WHERE dp.database_id = DB_ID() + ) BEGIN + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); + END; + '; - SELECT '#deadlock_data' AS table_name, * - FROM #deadlock_data AS dd - OPTION ( RECOMPILE ); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; - SELECT '#deadlock_resource' AS table_name, * - FROM #deadlock_resource AS dr - OPTION ( RECOMPILE ); + IF @Azure = 1 + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Inserting to @sysAssObjId at %s', 0, 1, @d) WITH NOWAIT; - SELECT '#deadlock_owner_waiter' AS table_name, * - FROM #deadlock_owner_waiter AS dow - OPTION ( RECOMPILE ); + INSERT INTO + @sysAssObjId + ( + database_id, + partition_id, + schema_name, + table_name + ) + SELECT + database_id = + DB_ID(), + p.partition_id, + schema_name = + s.name, + table_name = + t.name + FROM sys.partitions p + JOIN sys.tables t + ON t.object_id = p.object_id + JOIN sys.schemas s + ON s.schema_id = t.schema_id + WHERE s.name IS NOT NULL + AND t.name IS NOT NULL + OPTION(RECOMPILE); - SELECT '#deadlock_process' AS table_name, * - FROM #deadlock_process AS dp - OPTION ( RECOMPILE ); + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; - SELECT '#deadlock_stack' AS table_name, * - FROM #deadlock_stack AS ds - OPTION ( RECOMPILE ); - - END; -- End debug + /*Begin checks based on parsed values*/ - END; --Final End + /* + First, revert these back since we already converted the event data to local time, + and searches will break if we use the times converted over to UTC for the event data + */ + SELECT + @StartDate = @StartDateOriginal, + @EndDate = @EndDateOriginal; -GO + /*Check 1 is deadlocks by database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 1 database deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 1, + dp.database_name, + object_name = N'-', + finding_group = N'Total Database Deadlocks', + finding = + N'This database had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 2 is deadlocks with selects*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 2 select deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 2, + dow.database_name, + object_name = + CASE + WHEN EXISTS + ( + SELECT + 1/0 + FROM sys.databases AS d + WHERE d.name COLLATE DATABASE_DEFAULT = dow.database_name COLLATE DATABASE_DEFAULT + AND d.is_read_committed_snapshot_on = 1 + ) + THEN N'You already enabled RCSI, but...' + ELSE N'You Might Need RCSI' + END, + finding_group = N'Total Deadlocks Involving Selects', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s) between read queries and modification queries.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND dow.lock_mode IN + ( + N'S', + N'IS' + ) + OR dow.owner_mode IN + ( + N'S', + N'IS' + ) + OR dow.waiter_mode IN + ( + N'S', + N'IS' + ) + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 is deadlocks by object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 object deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + object_name = + ISNULL + ( + dow.object_name, + N'UNKNOWN' + ), + finding_group = N'Total Object Deadlocks', + finding = + N'This object was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + GROUP BY + dow.database_name, + dow.object_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 continuation, number of deadlocks per index*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) index deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Index Deadlocks', + finding = + N'This index was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type NOT IN + ( + N'HEAP', + N'RID' + ) + AND dow.index_name IS NOT NULL + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 3 continuation, number of deadlocks per heap*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 3 (continued) heap deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 3, + dow.database_name, + index_name = dow.index_name, + finding_group = N'Total Heap Deadlocks', + finding = + N'This heap was involved in ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dow.event_date) + ) + + N' deadlock(s).', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dow.event_date) DESC) + FROM #deadlock_owner_waiter AS dow + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.lock_type IN + ( + N'HEAP', + N'RID' + ) + GROUP BY + dow.database_name, + dow.index_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 4 looks for Serializable deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 4 serializable deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 4, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Serializable Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Serializable deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'serializable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 5 looks for Repeatable Read deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 5 repeatable read deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 5, + dp.database_name, + object_name = N'-', + finding_group = N'Repeatable Read Deadlocking', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of Repeatable Read deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + WHERE dp.isolation_level LIKE N'repeatable%' + 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) + GROUP BY dp.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 6 breaks down app, host, and login information*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 6 app/host/login deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 6, + database_name = + dp.database_name, + object_name = N'-', + finding_group = N'Login, App, and Host deadlocks', + finding = + N'This database has had ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' instances of deadlocks involving the login ' + + ISNULL + ( + dp.login_name, + N'UNKNOWN' + ) + + N' from the application ' + + ISNULL + ( + dp.client_app, + N'UNKNOWN' + ) + + N' on host ' + + ISNULL + ( + dp.host_name, + N'UNKNOWN' + ) + + N'.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT dp.event_date) DESC) + FROM #deadlock_process AS dp + 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) + GROUP BY + dp.database_name, + dp.login_name, + dp.client_app, + dp.host_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 7 breaks down the types of deadlocks (object, page, key, etc.)*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 7 types of deadlocks %s', 0, 1, @d) WITH NOWAIT; + + WITH + lock_types AS + ( + SELECT + dp.database_name, + dow.object_name, + lock = + CASE + WHEN CHARINDEX(N':', dp.wait_resource) > 0 + 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)) + 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 (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 LEFT(dp.wait_resource, CHARINDEX(N':', dp.wait_resource) - 1) + ELSE dp.wait_resource + END + ) + INSERT #deadlock_findings WITH (TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 7, + lt.database_name, + lt.object_name, + finding_group = N'Types of locks by object', + finding = + N'This object has had ' + + STUFF( + ( + SELECT + 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(''), TYPE + ).value('.', 'nvarchar(max)'), + 1, 2, N'' + ) + N' locks.', + sort_order = + ROW_NUMBER() OVER ( + ORDER BY + MAX(CONVERT(bigint, lt.lock_count)) DESC + ) + FROM lock_types AS lt + GROUP BY + lt.database_name, + lt.object_name + OPTION (RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 8 gives you more info queries for sp_BlitzCache & BlitzQueryStore*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 1 BlitzCache %s', 0, 1, @d) WITH NOWAIT; + + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.event_date, + ds.proc_name, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1), + sql_handle_csv = + N'''' + + STUFF + ( + ( + SELECT DISTINCT + N',' + + ds2.sql_handle + FROM #deadlock_stack AS ds2 + WHERE ds2.id = ds.id + AND ds2.event_date = ds.event_date + AND ds2.sql_handle <> 0x + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(MAX)'), + 1, + 1, + N'' + ) + + N'''' + FROM #deadlock_stack AS ds + WHERE ds.sql_handle <> 0x + GROUP BY + PARSENAME(ds.proc_name, 3), + PARSENAME(ds.proc_name, 2), + PARSENAME(ds.proc_name, 1), + ds.proc_name, + ds.id, + ds.event_date + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = N'EXECUTE sp_BlitzCache ' + + CASE + WHEN ds.proc_name = N'adhoc' + THEN N'@OnlySqlHandles = ' + ds.sql_handle_csv + ELSE N'@StoredProcName = ' + QUOTENAME(ds.proc_only_name, N'''') + END + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + AND ds.proc_name NOT LIKE 'Unknown%' + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + IF (@ProductVersionMajor >= 13 OR @Azure = 1) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 8 more info part 2 BlitzQueryStore %s', 0, 1, @d) WITH NOWAIT; + + WITH + deadlock_stack AS + ( + SELECT DISTINCT + ds.id, + ds.sql_handle, + ds.proc_name, + ds.event_date, + database_name = + PARSENAME(ds.proc_name, 3), + schema_name = + PARSENAME(ds.proc_name, 2), + proc_only_name = + PARSENAME(ds.proc_name, 1) + FROM #deadlock_stack AS ds + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 8, + dow.database_name, + object_name = ds.proc_name, + finding_group = N'More Info - Query', + finding = + N'EXECUTE sp_BlitzQueryStore ' + + N'@DatabaseName = ' + + QUOTENAME(ds.database_name, N'''') + + N', ' + + N'@StoredProcName = ' + + QUOTENAME(ds.proc_only_name, N'''') + + N';' + FROM deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + WHERE ds.proc_name <> N'adhoc' + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @StoredProcName OR @StoredProcName IS NULL) + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + END; + + /*Check 9 gives you stored procedure deadlock counts*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 9 stored procedure deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 9, + database_name = + dp.database_name, + object_name = ds.proc_name, + finding_group = N'Stored Procedure Deadlocks', + finding = + N'The stored procedure ' + + PARSENAME(ds.proc_name, 2) + + N'.' + + PARSENAME(ds.proc_name, 1) + + N' has been involved in ' + + CONVERT + ( + nvarchar(10), + COUNT_BIG(DISTINCT ds.id) + ) + + N' deadlocks.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT ds.id) DESC) + FROM #deadlock_stack AS ds + JOIN #deadlock_process AS dp + ON dp.id = ds.id + AND ds.event_date = dp.event_date + WHERE ds.proc_name <> N'adhoc' + AND (ds.proc_name = @StoredProcName OR @StoredProcName IS NULL) + 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) + GROUP BY + dp.database_name, + ds.proc_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 10 gives you more info queries for sp_BlitzIndex */ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 10 more info, BlitzIndex %s', 0, 1, @d) WITH NOWAIT; + + WITH + bi AS + ( + SELECT DISTINCT + dow.object_name, + dow.database_name, + schema_name = s.schema_name, + table_name = s.table_name + FROM #deadlock_owner_waiter AS dow + JOIN @sysAssObjId AS s + ON s.database_id = dow.database_id + AND s.partition_id = dow.associatedObjectId + WHERE 1 = 1 + AND (dow.database_id = @DatabaseId OR @DatabaseName IS NULL) + AND (dow.event_date >= @StartDate OR @StartDate IS NULL) + AND (dow.event_date < @EndDate OR @EndDate IS NULL) + AND (dow.object_name = @ObjectName OR @ObjectName IS NULL) + AND dow.object_name IS NOT NULL + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT DISTINCT + check_id = 10, + bi.database_name, + bi.object_name, + finding_group = N'More Info - Table', + finding = + N'EXECUTE sp_BlitzIndex ' + + N'@DatabaseName = ' + + QUOTENAME(bi.database_name, N'''') + + N', @SchemaName = ' + + QUOTENAME(bi.schema_name, N'''') + + N', @TableName = ' + + QUOTENAME(bi.table_name, N'''') + + N';' + FROM bi + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + /*Check 11 gets total deadlock wait time per object*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 11 deadlock wait time per object %s', 0, 1, @d) WITH NOWAIT; + + WITH + chopsuey AS + ( + + SELECT + database_name = + dp.database_name, + dow.object_name, + wait_days = + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) / 1000 / 86400 + ) + ), + wait_time_hms = + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + ), + 0 + ), + 14 + ) + END, + total_waits = + SUM(CONVERT(bigint, dp.wait_time)) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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 (dow.object_name = @ObjectName OR @ObjectName 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) + GROUP BY + dp.database_name, + dow.object_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 11, + cs.database_name, + cs.object_name, + finding_group = N'Total object deadlock wait time', + finding = + N'This object has had ' + + CONVERT + ( + nvarchar(30), + cs.wait_days + ) + + N' ' + + CONVERT + ( + nvarchar(30), + cs.wait_time_hms, + 14 + ) + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY cs.total_waits DESC) + FROM chopsuey AS cs + WHERE cs.object_name IS NOT NULL + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 12 gets total deadlock wait time per database*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 12 deadlock wait time per database %s', 0, 1, @d) WITH NOWAIT; + + WITH + wait_time AS + ( + SELECT + database_name = + dp.database_name, + total_wait_time_ms = + SUM + ( + CONVERT + ( + bigint, + dp.wait_time + ) + ) + FROM #deadlock_owner_waiter AS dow + JOIN #deadlock_process AS dp + 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) + GROUP BY + dp.database_name + ) + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 12, + wt.database_name, + object_name = N'-', + finding_group = N'Total database deadlock wait time', + N'This database has had ' + + CONVERT + ( + nvarchar(30), + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) / 1000 / 86400 + ) + ) + + N' ' + + /*the more wait time you rack up the less accurate this gets, + it's either that or erroring out*/ + CASE + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + )/1000 > 2147483647 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + MINUTE, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 60000 + ), + 0 + ), + 14 + ) + WHEN + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) BETWEEN 2147483648 AND 2147483647000 + THEN + CONVERT + ( + nvarchar(30), + DATEADD + ( + SECOND, + ( + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + )/ + 1000 + ), + 0 + ), + 14 + ) + ELSE + CONVERT + ( + nvarchar(30), + DATEADD + ( + MILLISECOND, + ( + SUM + ( + CONVERT + ( + bigint, + wt.total_wait_time_ms + ) + ) + ), + 0 + ), + 14 + ) END + + N' [dd hh:mm:ss:ms] of deadlock wait time.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY SUM(CONVERT(bigint, wt.total_wait_time_ms)) DESC) + FROM wait_time AS wt + GROUP BY + wt.database_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 13 gets total deadlock wait time for SQL Agent*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 13 deadlock count for SQL Agent %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding, + sort_order + ) + SELECT + check_id = 13, + database_name = + DB_NAME(aj.database_id), + object_name = + N'SQLAgent - Job: ' + + aj.job_name + + N' Step: ' + + aj.step_name, + finding_group = N'Agent Job Deadlocks', + finding = + N'There have been ' + + RTRIM(COUNT_BIG(DISTINCT aj.event_date)) + + N' deadlocks from this Agent Job and Step.', + sort_order = + ROW_NUMBER() + OVER (ORDER BY COUNT_BIG(DISTINCT aj.event_date) DESC) + FROM #agent_job AS aj + GROUP BY + DB_NAME(aj.database_id), + aj.job_name, + aj.step_name + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 14 is total parallel deadlocks*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 14 parallel deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total parallel deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT drp.event_date) + ) + + N' parallel deadlocks.' + FROM #deadlock_resource_parallel AS drp + HAVING COUNT_BIG(DISTINCT drp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 15 is total deadlocks involving sleeping sessions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 15 sleeping and background deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving sleeping sessions', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' sleepy deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.status = N'sleeping' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 15, + database_name = N'-', + object_name = N'-', + finding_group = N'Total deadlocks involving background processes', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' deadlocks with background task.' + FROM #deadlock_process AS dp + WHERE dp.status = N'background' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Check 16 is total deadlocks involving implicit transactions*/ + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Check 16 implicit transaction deadlocks %s', 0, 1, @d) WITH NOWAIT; + + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + check_id = 14, + database_name = N'-', + object_name = N'-', + finding_group = N'Total implicit transaction deadlocks', + finding = + N'There have been ' + + CONVERT + ( + nvarchar(20), + COUNT_BIG(DISTINCT dp.event_date) + ) + + N' implicit transaction deadlocks.' + FROM #deadlock_process AS dp + WHERE dp.transaction_name = N'implicit_transaction' + HAVING COUNT_BIG(DISTINCT dp.event_date) > 0 + OPTION(RECOMPILE); + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*Thank you goodnight*/ + INSERT + #deadlock_findings WITH(TABLOCKX) + ( + check_id, + database_name, + object_name, + finding_group, + finding + ) + VALUES + ( + -1, + N'sp_BlitzLock version ' + CONVERT(nvarchar(10), @Version), + N'Results for ' + CONVERT(nvarchar(10), @StartDate, 23) + N' through ' + CONVERT(nvarchar(10), @EndDate, 23), + N'http://FirstResponderKit.org/', + N'To get help or add your own contributions to the SQL Server First Responder Kit, join us at http://FirstResponderKit.org.' + ); + + RAISERROR('Finished rollup at %s', 0, 1, @d) WITH NOWAIT; + + /*Results*/ + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results 1 %s', 0, 1, @d) WITH NOWAIT; + + CREATE CLUSTERED INDEX cx_whatever_dp ON #deadlock_process (event_date, id); + CREATE CLUSTERED INDEX cx_whatever_drp ON #deadlock_resource_parallel (event_date, owner_id); + CREATE CLUSTERED INDEX cx_whatever_dow ON #deadlock_owner_waiter (event_date, owner_id, waiter_id); + + IF @Debug = 1 BEGIN SET STATISTICS XML ON; END; + + WITH + deadlocks AS + ( + SELECT + deadlock_type = + N'Regular Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + wait_resource = + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + dp.is_victim, + owner_mode = + ISNULL(dp.owner_mode, N'-'), + owner_waiter_type = NULL, + owner_activity = NULL, + owner_waiter_activity = NULL, + owner_merging = NULL, + owner_spilling = NULL, + owner_waiting_to_close = NULL, + waiter_mode = + ISNULL(dp.waiter_mode, N'-'), + waiter_waiter_type = NULL, + waiter_owner_activity = NULL, + waiter_waiter_activity = NULL, + waiter_merging = NULL, + waiter_spilling = NULL, + waiter_waiting_to_close = NULL, + dp.deadlock_graph + FROM #deadlock_process AS dp + WHERE dp.victim_id IS NOT NULL + AND dp.is_parallel = 0 + + UNION ALL + + SELECT + deadlock_type = + N'Parallel Deadlock', + dp.event_date, + dp.id, + dp.victim_id, + dp.spid, + dp.database_id, + dp.database_name, + dp.current_database_name, + dp.priority, + dp.log_used, + dp.wait_resource COLLATE DATABASE_DEFAULT, + object_names = + CONVERT + ( + xml, + STUFF + ( + ( + SELECT DISTINCT + object_name = + NCHAR(10) + + N' ' + + ISNULL(c.object_name, N'') + + N' ' COLLATE DATABASE_DEFAULT + FROM #deadlock_owner_waiter AS c + WHERE (dp.id = c.owner_id + OR dp.victim_id = c.waiter_id) + AND dp.event_date = c.event_date + FOR XML + PATH(N''), + TYPE + ).value(N'.[1]', N'nvarchar(4000)'), + 1, + 1, + N'' + ) + ), + dp.wait_time, + dp.transaction_name, + dp.status, + dp.last_tran_started, + dp.last_batch_started, + dp.last_batch_completed, + dp.lock_mode, + dp.transaction_count, + dp.client_app, + dp.host_name, + dp.login_name, + dp.isolation_level, + dp.client_option_1, + dp.client_option_2, + inputbuf = + dp.process_xml.value('(//process/inputbuf/text())[1]', 'nvarchar(MAX)'), + en = + DENSE_RANK() OVER (ORDER BY dp.event_date), + qn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date ORDER BY dp.event_date) - 1, + dn = + ROW_NUMBER() OVER (PARTITION BY dp.event_date, dp.id ORDER BY dp.event_date), + is_victim = 1, + owner_mode = cao.wait_type COLLATE DATABASE_DEFAULT, + owner_waiter_type = cao.waiter_type, + owner_activity = cao.owner_activity, + owner_waiter_activity = cao.waiter_activity, + owner_merging = cao.merging, + owner_spilling = cao.spilling, + owner_waiting_to_close = cao.waiting_to_close, + waiter_mode = caw.wait_type COLLATE DATABASE_DEFAULT, + waiter_waiter_type = caw.waiter_type, + waiter_owner_activity = caw.owner_activity, + waiter_waiter_activity = caw.waiter_activity, + waiter_merging = caw.merging, + waiter_spilling = caw.spilling, + waiter_waiting_to_close = caw.waiting_to_close, + dp.deadlock_graph + FROM #deadlock_process AS dp + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeNewRow' + ) + ORDER BY drp.event_date + ) AS cao + OUTER APPLY + ( + SELECT TOP (1) + drp.* + FROM #deadlock_resource_parallel AS drp + WHERE drp.owner_id = dp.id + AND drp.wait_type IN + ( + N'e_waitPortOpen', + N'e_waitPipeGetRow' + ) + ORDER BY drp.event_date + ) AS caw + WHERE dp.is_parallel = 1 + ) + SELECT + d.deadlock_type, + d.event_date, + d.id, + d.victim_id, + d.spid, + deadlock_group = + N'Deadlock #' + + CONVERT + ( + nvarchar(10), + d.en + ) + + N', Query #' + + CASE + WHEN d.qn = 0 + THEN N'1' + ELSE CONVERT(nvarchar(10), d.qn) + END + CASE + WHEN d.is_victim = 1 + THEN N' - VICTIM' + ELSE N'' + END, + d.database_id, + d.database_name, + d.current_database_name, + d.priority, + d.log_used, + d.wait_resource, + d.object_names, + d.wait_time, + d.transaction_name, + d.status, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.lock_mode, + d.transaction_count, + d.client_app, + d.host_name, + d.login_name, + d.isolation_level, + d.client_option_1, + d.client_option_2, + inputbuf = + CASE + WHEN d.inputbuf + LIKE @inputbuf_bom + N'Proc |[Database Id = %' ESCAPE N'|' + THEN + OBJECT_SCHEMA_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + + N'.' + + OBJECT_NAME + ( + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Object Id = ', d.inputbuf) + 12, + LEN(d.inputbuf) - (CHARINDEX(N'Object Id = ', d.inputbuf) + 12) + ) + , + SUBSTRING + ( + d.inputbuf, + CHARINDEX(N'Database Id = ', d.inputbuf) + 14, + CHARINDEX(N'Object Id', d.inputbuf) - (CHARINDEX(N'Database Id = ', d.inputbuf) + 14) + ) + ) + ELSE d.inputbuf + END COLLATE Latin1_General_BIN2, + d.owner_mode, + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_mode, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + d.deadlock_graph, + d.is_victim + INTO #deadlocks + FROM deadlocks AS d + WHERE d.dn = 1 + AND (d.is_victim = @VictimsOnly + OR @VictimsOnly = 0) + AND d.qn < CASE + WHEN d.deadlock_type = N'Parallel Deadlock' + THEN 2 + ELSE 2147483647 + END + AND (DB_NAME(d.database_id) = @DatabaseName OR @DatabaseName IS NULL) + AND (d.event_date >= @StartDate OR @StartDate IS NULL) + AND (d.event_date < @EndDate OR @EndDate IS NULL) + AND (CONVERT(nvarchar(MAX), d.object_names) COLLATE Latin1_General_BIN2 LIKE N'%' + @ObjectName + N'%' OR @ObjectName IS NULL) + AND (d.client_app = @AppName OR @AppName IS NULL) + AND (d.host_name = @HostName OR @HostName IS NULL) + AND (d.login_name = @LoginName OR @LoginName IS NULL) + AND (d.deadlock_type = @DeadlockType OR @DeadlockType IS NULL) + OPTION (RECOMPILE, LOOP JOIN, HASH JOIN); + + UPDATE d + SET d.inputbuf = + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE( + d.inputbuf, + NCHAR(31), N'?'), NCHAR(30), N'?'), NCHAR(29), N'?'), NCHAR(28), N'?'), NCHAR(27), N'?'), + NCHAR(26), N'?'), NCHAR(25), N'?'), NCHAR(24), N'?'), NCHAR(23), N'?'), NCHAR(22), N'?'), + NCHAR(21), N'?'), NCHAR(20), N'?'), NCHAR(19), N'?'), NCHAR(18), N'?'), NCHAR(17), N'?'), + NCHAR(16), N'?'), NCHAR(15), N'?'), NCHAR(14), N'?'), NCHAR(12), N'?'), NCHAR(11), N'?'), + NCHAR(8), N'?'), NCHAR(7), N'?'), NCHAR(6), N'?'), NCHAR(5), N'?'), NCHAR(4), N'?'), + NCHAR(3), N'?'), NCHAR(2), N'?'), NCHAR(1), N'?'), NCHAR(0), N'?') + FROM #deadlocks AS d + OPTION(RECOMPILE); + + SELECT + d.deadlock_type, + d.event_date, + database_name = + DB_NAME(d.database_id), + database_name_x = + d.database_name, + d.current_database_name, + d.spid, + d.deadlock_group, + d.client_option_1, + d.client_option_2, + d.lock_mode, + query_xml = + ( + SELECT + [processing-instruction(query)] = + d.inputbuf + FOR XML + PATH(N''), + TYPE + ), + query_string = + d.inputbuf, + d.object_names, + d.isolation_level, + d.owner_mode, + d.waiter_mode, + d.transaction_count, + d.login_name, + d.host_name, + d.client_app, + d.wait_time, + d.wait_resource, + d.priority, + d.log_used, + d.last_tran_started, + d.last_batch_started, + d.last_batch_completed, + d.transaction_name, + d.status, + /*These columns will be NULL for regular (non-parallel) deadlocks*/ + parallel_deadlock_details = + ( + SELECT + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close + FOR XML + PATH('parallel_deadlock_details'), + TYPE + ), + d.owner_waiter_type, + d.owner_activity, + d.owner_waiter_activity, + d.owner_merging, + d.owner_spilling, + d.owner_waiting_to_close, + d.waiter_waiter_type, + d.waiter_owner_activity, + d.waiter_waiter_activity, + d.waiter_merging, + d.waiter_spilling, + d.waiter_waiting_to_close, + /*end parallel deadlock columns*/ + d.deadlock_graph, + d.is_victim, + d.id + INTO #deadlock_results + FROM #deadlocks AS d; + + IF @Debug = 1 BEGIN SET STATISTICS XML OFF; END; + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + /*There's too much risk of errors sending the*/ + IF @OutputDatabaseCheck = 0 + BEGIN + SET @ExportToExcel = 0; + END; + + SET @deadlock_result += N' + SELECT + server_name = + @@SERVERNAME, + dr.deadlock_type, + dr.event_date, + database_name = + COALESCE + ( + dr.database_name, + dr.database_name_x, + dr.current_database_name + ), + dr.spid, + dr.deadlock_group, + ' + CASE @ExportToExcel + WHEN 1 + THEN N' + query = dr.query_string, + object_names = + REPLACE( + REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.object_names + ) COLLATE Latin1_General_BIN2, + '''', ''''), + '''', ''''),' + ELSE N'query = dr.query_xml, + dr.object_names,' + END + N' + dr.isolation_level, + dr.owner_mode, + dr.waiter_mode, + dr.lock_mode, + dr.transaction_count, + dr.client_option_1, + dr.client_option_2, + dr.login_name, + dr.host_name, + dr.client_app, + dr.wait_time, + dr.wait_resource, + dr.priority, + dr.log_used, + dr.last_tran_started, + dr.last_batch_started, + dr.last_batch_completed, + dr.transaction_name, + dr.status,' + + CASE + WHEN (@ExportToExcel = 1 + OR @OutputDatabaseCheck = 0) + THEN N' + dr.owner_waiter_type, + dr.owner_activity, + dr.owner_waiter_activity, + dr.owner_merging, + dr.owner_spilling, + dr.owner_waiting_to_close, + dr.waiter_waiter_type, + dr.waiter_owner_activity, + dr.waiter_waiter_activity, + dr.waiter_merging, + dr.waiter_spilling, + dr.waiter_waiting_to_close,' + ELSE N' + dr.parallel_deadlock_details,' + END + + CASE + @ExportToExcel + WHEN 1 + THEN N' + deadlock_graph = + REPLACE(REPLACE( + REPLACE(REPLACE( + CONVERT + ( + nvarchar(MAX), + dr.deadlock_graph + ) COLLATE Latin1_General_BIN2, + ''NCHAR(10)'', ''''), ''NCHAR(13)'', ''''), + ''CHAR(10)'', ''''), ''CHAR(13)'', '''')' + ELSE N' + dr.deadlock_graph' + END + N' + FROM #deadlock_results AS dr + ORDER BY + dr.event_date, + dr.is_victim DESC + OPTION(RECOMPILE, LOOP JOIN, HASH JOIN); + '; + + IF (@OutputDatabaseCheck = 0) + BEGIN + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Results to table %s', 0, 1, @d) WITH NOWAIT; + + IF @Debug = 1 + BEGIN + PRINT @deadlock_result; + SET STATISTICS XML ON; + END; + + SET @StringToExecute = N' + + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadLockTbl + ( + ServerName, + deadlock_type, + event_date, + database_name, + spid, + deadlock_group, + query, + object_names, + isolation_level, + owner_mode, + waiter_mode, + lock_mode, + transaction_count, + client_option_1, + client_option_2, + login_name, + host_name, + client_app, + wait_time, + wait_resource, + priority, + log_used, + last_tran_started, + last_batch_started, + last_batch_completed, + transaction_name, + status, + owner_waiter_type, + owner_activity, + owner_waiter_activity, + owner_merging, + owner_spilling, + owner_waiting_to_close, + waiter_waiter_type, + waiter_owner_activity, + waiter_waiter_activity, + waiter_merging, + waiter_spilling, + waiter_waiting_to_close, + deadlock_graph + ) + EXECUTE sys.sp_executesql + @deadlock_result;'; + EXECUTE sys.sp_executesql @StringToExecute, N'@deadlock_result NVARCHAR(MAX)', @deadlock_result; + + IF @Debug = 1 + BEGIN + SET STATISTICS XML OFF; + END; + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + DROP SYNONYM dbo.DeadLockTbl; + + SET @d = CONVERT(varchar(40), GETDATE(), 109); + RAISERROR('Findings to table %s', 0, 1, @d) WITH NOWAIT; + + SET @StringToExecute = N' + + INSERT INTO ' + QUOTENAME(DB_NAME()) + N'..DeadlockFindings + ( + ServerName, + check_id, + database_name, + object_name, + finding_group, + finding + ) + SELECT + @@SERVERNAME, + df.check_id, + df.database_name, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY df.check_id + OPTION(RECOMPILE);'; + EXECUTE sys.sp_executesql @StringToExecute; + + RAISERROR('Finished at %s', 0, 1, @d) WITH NOWAIT; + + DROP SYNONYM dbo.DeadlockFindings; /*done with inserting.*/ + END; + ELSE /*Output to database is not set output to client app*/ + 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; + + 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', + ds.proc_name, + sql_handle = + CONVERT(varbinary(64), ds.sql_handle, 1), + dow.database_name, + dow.database_id, + dow.object_name, + query_xml = + TRY_CAST(dr.query_xml AS nvarchar(MAX)) + INTO #available_plans + FROM #deadlock_stack AS ds + JOIN #deadlock_owner_waiter AS dow + ON dow.owner_id = ds.id + AND dow.event_date = ds.event_date + JOIN #deadlock_results AS dr + ON dr.id = ds.id + AND dr.event_date = ds.event_date + OPTION(RECOMPILE); + + SELECT + deqs.sql_handle, + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset, + deqs.creation_time, + deqs.last_execution_time, + deqs.execution_count, + total_worker_time_ms = + deqs.total_worker_time / 1000., + avg_worker_time_ms = + CONVERT(decimal(38, 6), deqs.total_worker_time / 1000. / deqs.execution_count), + total_elapsed_time_ms = + deqs.total_elapsed_time / 1000., + avg_elapsed_time_ms = + CONVERT(decimal(38, 6), deqs.total_elapsed_time / 1000. / deqs.execution_count), + executions_per_second = + ISNULL + ( + deqs.execution_count / + NULLIF + ( + DATEDIFF + ( + SECOND, + deqs.creation_time, + NULLIF(deqs.last_execution_time, '1900-01-01 00:00:00.000') + ), + 0 + ), + 0 + ), + total_physical_reads_mb = + deqs.total_physical_reads * 8. / 1024., + total_logical_writes_mb = + deqs.total_logical_writes * 8. / 1024., + total_logical_reads_mb = + deqs.total_logical_reads * 8. / 1024., + min_grant_mb = + deqs.min_grant_kb * 8. / 1024., + max_grant_mb = + deqs.max_grant_kb * 8. / 1024., + min_used_grant_mb = + deqs.min_used_grant_kb * 8. / 1024., + max_used_grant_mb = + 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 = + deqs.max_worker_time / 1000., + max_elapsed_time_ms = + deqs.max_elapsed_time / 1000. + INTO #dm_exec_query_stats + FROM sys.dm_exec_query_stats AS deqs + WHERE EXISTS + ( + SELECT + 1/0 + FROM #available_plans AS ap + WHERE ap.sql_handle = deqs.sql_handle + ) + AND deqs.query_hash IS NOT NULL; + + CREATE CLUSTERED INDEX + deqs + ON #dm_exec_query_stats + ( + sql_handle, + plan_handle + ); + + SELECT + ap.available_plans, + ap.database_name, + query_text = + TRY_CAST(ap.query_xml AS xml), + ap.query_plan, + ap.creation_time, + ap.last_execution_time, + ap.execution_count, + ap.executions_per_second, + ap.total_worker_time_ms, + ap.avg_worker_time_ms, + ap.max_worker_time_ms, + ap.total_elapsed_time_ms, + ap.avg_elapsed_time_ms, + ap.max_elapsed_time_ms, + ap.total_logical_reads_mb, + ap.total_physical_reads_mb, + ap.total_logical_writes_mb, + ap.min_grant_mb, + ap.max_grant_mb, + ap.min_used_grant_mb, + ap.max_used_grant_mb, + ap.min_reserved_threads, + ap.max_reserved_threads, + ap.min_used_threads, + ap.max_used_threads, + ap.total_rows, + ap.sql_handle, + ap.statement_start_offset, + ap.statement_end_offset + FROM + ( + + SELECT + ap.*, + c.statement_start_offset, + c.statement_end_offset, + c.creation_time, + c.last_execution_time, + c.execution_count, + c.total_worker_time_ms, + c.avg_worker_time_ms, + c.total_elapsed_time_ms, + c.avg_elapsed_time_ms, + c.executions_per_second, + c.total_physical_reads_mb, + c.total_logical_writes_mb, + c.total_logical_reads_mb, + c.min_grant_mb, + c.max_grant_mb, + c.min_used_grant_mb, + c.max_used_grant_mb, + c.min_reserved_threads, + c.max_reserved_threads, + c.min_used_threads, + c.max_used_threads, + c.total_rows, + c.query_plan, + c.max_worker_time_ms, + c.max_elapsed_time_ms + FROM #available_plans AS ap + OUTER APPLY + ( + SELECT + deqs.*, + query_plan = + TRY_CAST(deps.query_plan AS xml) + FROM #dm_exec_query_stats deqs + OUTER APPLY sys.dm_exec_text_query_plan + ( + deqs.plan_handle, + deqs.statement_start_offset, + deqs.statement_end_offset + ) AS deps + WHERE deqs.sql_handle = ap.sql_handle + AND deps.dbid = ap.database_id + ) AS c + ) AS ap + WHERE ap.query_plan IS NOT NULL + ORDER BY + ap.avg_worker_time_ms DESC + 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, + df.object_name, + df.finding_group, + df.finding + FROM #deadlock_findings AS df + ORDER BY + 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.*/ + END; + + IF @Debug = 1 + BEGIN + SELECT + table_name = N'#deadlock_data', + * + FROM #deadlock_data AS dd + OPTION(RECOMPILE); + + SELECT + table_name = N'#dd', + * + FROM #dd AS d + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_resource', + * + FROM #deadlock_resource AS dr + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_resource_parallel', + * + FROM #deadlock_resource_parallel AS drp + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_owner_waiter', + * + FROM #deadlock_owner_waiter AS dow + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_process', + * + FROM #deadlock_process AS dp + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_stack', + * + FROM #deadlock_stack AS ds + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlocks', + * + FROM #deadlocks AS d + OPTION(RECOMPILE); + + SELECT + table_name = N'#deadlock_results', + * + FROM #deadlock_results AS dr + OPTION(RECOMPILE); + + SELECT + table_name = N'#x', + * + FROM #x AS x + OPTION(RECOMPILE); + + SELECT + table_name = N'@sysAssObjId', + * + FROM @sysAssObjId AS s + OPTION(RECOMPILE); + + IF OBJECT_ID('tempdb..#available_plans') IS NOT NULL + BEGIN + SELECT + table_name = N'#available_plans', + * + FROM #available_plans AS ap + OPTION(RECOMPILE); + END; + + IF OBJECT_ID('tempdb..#dm_exec_query_stats') IS NOT NULL + BEGIN + SELECT + table_name = N'#dm_exec_query_stats', + * + FROM #dm_exec_query_stats + OPTION(RECOMPILE); + END; + + SELECT + procedure_parameters = + 'procedure_parameters', + DatabaseName = + @DatabaseName, + StartDate = + @StartDate, + EndDate = + @EndDate, + ObjectName = + @ObjectName, + StoredProcName = + @StoredProcName, + AppName = + @AppName, + HostName = + @HostName, + LoginName = + @LoginName, + EventSessionName = + @EventSessionName, + TargetSessionType = + @TargetSessionType, + VictimsOnly = + @VictimsOnly, + DeadlockType = + @DeadlockType, + TargetDatabaseName = + @TargetDatabaseName, + TargetSchemaName = + @TargetSchemaName, + TargetTableName = + @TargetTableName, + TargetColumnName = + @TargetColumnName, + TargetTimestampColumnName = + @TargetTimestampColumnName, + Debug = + @Debug, + Help = + @Help, + Version = + @Version, + VersionDate = + @VersionDate, + VersionCheckMode = + @VersionCheckMode, + OutputDatabaseName = + @OutputDatabaseName, + OutputSchemaName = + @OutputSchemaName, + OutputTableName = + @OutputTableName, + ExportToExcel = + @ExportToExcel; + + SELECT + declared_variables = + 'declared_variables', + DatabaseId = + @DatabaseId, + StartDateUTC = + @StartDateUTC, + EndDateUTC = + @EndDateUTC, + ProductVersion = + @ProductVersion, + ProductVersionMajor = + @ProductVersionMajor, + ProductVersionMinor = + @ProductVersionMinor, + ObjectFullName = + @ObjectFullName, + Azure = + @Azure, + RDS = + @RDS, + d = + @d, + StringToExecute = + @StringToExecute, + StringToExecuteParams = + @StringToExecuteParams, + r = + @r, + OutputTableFindings = + @OutputTableFindings, + DeadlockCount = + @DeadlockCount, + ServerName = + @ServerName, + OutputDatabaseCheck = + @OutputDatabaseCheck, + SessionId = + @SessionId, + TargetSessionId = + @TargetSessionId, + FileName = + @FileName, + inputbuf_bom = + @inputbuf_bom, + deadlock_result = + @deadlock_result; + END; /*End debug*/ + END; /*Final End*/ +GO diff --git a/sp_BlitzWho.sql b/sp_BlitzWho.sql index c7e50c5a6..64df0ca84 100644 --- a/sp_BlitzWho.sql +++ b/sp_BlitzWho.sql @@ -7,17 +7,43 @@ ALTER PROCEDURE dbo.sp_BlitzWho @ShowSleepingSPIDs TINYINT = 0, @ExpertMode BIT = 0, @Debug BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @OutputDatabaseName NVARCHAR(256) = NULL , + @OutputSchemaName NVARCHAR(256) = NULL , + @OutputTableName NVARCHAR(256) = NULL , + @OutputTableRetentionDays TINYINT = 3 , + @MinElapsedSeconds INT = 0 , + @MinCPUTime INT = 0 , + @MinLogicalReads INT = 0 , + @MinPhysicalReads INT = 0 , + @MinWrites INT = 0 , + @MinTempdbMB INT = 0 , + @MinRequestedMemoryKB INT = 0 , + @MinBlockingSeconds INT = 0 , + @CheckDateOverride DATETIMEOFFSET = NULL, + @ShowActualParameters BIT = 0, + @GetOuterCommand BIT = 0, + @GetLiveQueryPlan BIT = NULL, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @SortOrder NVARCHAR(256) = N'elapsed time' AS BEGIN SET NOCOUNT ON; + SET STATISTICS XML OFF; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - DECLARE @Version VARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; + + SELECT @Version = '8.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; + IF @Help = 1 + BEGIN PRINT ' sp_BlitzWho from http://FirstResponderKit.org @@ -29,10 +55,13 @@ the findings, contribute your own code, and more. Known limitations of this version: - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Outputting to table is only supported with SQL Server 2012 and higher. + - If @OutputDatabaseName and @OutputSchemaName are populated, the database and + schema must already exist. We will not create them, only the table. MIT License -Copyright (c) 2017 Brent Ozar Unlimited +Copyright (c) Brent Ozar Unlimited Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -52,83 +81,533 @@ 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. '; +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]) + ,@AzureSQLDB BIT = (SELECT CASE WHEN SERVERPROPERTY('EngineEdition') = 5 THEN 1 ELSE 0 END) ,@EnhanceFlag BIT = 0 + ,@BlockingCheck NVARCHAR(MAX) + ,@StringToSelect NVARCHAR(MAX) ,@StringToExecute NVARCHAR(MAX) - ,@EnhanceSQL NVARCHAR(MAX) = - N'query_stats.last_dop, - query_stats.min_dop, - query_stats.max_dop, - query_stats.last_grant_kb, - query_stats.min_grant_kb, - query_stats.max_grant_kb, - query_stats.last_used_grant_kb, - query_stats.min_used_grant_kb, - query_stats.max_used_grant_kb, - query_stats.last_ideal_grant_kb, - query_stats.min_ideal_grant_kb, - query_stats.max_ideal_grant_kb, - query_stats.last_reserved_threads, - query_stats.min_reserved_threads, - query_stats.max_reserved_threads, - query_stats.last_used_threads, - query_stats.min_used_threads, - query_stats.max_used_threads,' + ,@OutputTableCleanupDate DATE ,@SessionWaits BIT = 0 ,@SessionWaitsSQL NVARCHAR(MAX) = N'LEFT JOIN ( SELECT DISTINCT wait.session_id , - ( SELECT TOP 5 waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_time_ms) AS NVARCHAR(128)) - + N'' ms), '' - FROM sys.dm_exec_session_wait_stats AS waitwait - WHERE waitwait.session_id = wait.session_id + ( SELECT TOP 5 waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_time_ms) AS NVARCHAR(128)) + + N'' ms), '' + FROM sys.dm_exec_session_wait_stats AS waitwait + WHERE waitwait.session_id = wait.session_id GROUP BY waitwait.wait_type HAVING SUM(waitwait.wait_time_ms) > 5 - ORDER BY SUM(waitwait.wait_time_ms) DESC + ORDER BY 1 FOR XML PATH('''') ) AS session_wait_info - FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 - ON s.session_id = wt2.session_id + FROM sys.dm_exec_session_wait_stats AS wait ) AS wt2 + ON s.session_id = wt2.session_id LEFT JOIN sys.dm_exec_query_stats AS session_stats - ON r.sql_handle = session_stats.sql_handle + ON r.sql_handle = session_stats.sql_handle AND r.plan_handle = session_stats.plan_handle - AND r.statement_start_offset = session_stats.statement_start_offset - AND r.statement_end_offset = session_stats.statement_end_offset' - ,@QueryStatsXML BIT = 0 - ,@QueryStatsXMLselect NVARCHAR(MAX) = N' CAST(COALESCE(qs_live.query_plan, '''') AS XML) AS live_query_plan , ' - ,@QueryStatsXMLSQL NVARCHAR(MAX) = N'OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live' + AND r.statement_start_offset = session_stats.statement_start_offset + AND r.statement_end_offset = session_stats.statement_end_offset' + ,@ObjectFullName NVARCHAR(2000) + ,@OutputTableNameQueryStats_View NVARCHAR(256) + ,@LineFeed NVARCHAR(MAX) /* Had to set as MAX up from 10 as it was truncating the view creation*/; +/* 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) -IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan') - SET @QueryStatsXML = 1; + @ProductVersionMinor = PARSENAME(CONVERT(VARCHAR(32), @ProductVersion), 2) +SELECT + @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 @ProductVersionMajor > 9 and @ProductVersionMajor < 11 +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 @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 + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @ExpertMode = 1; /* Force ExpertMode when we're logging to table */ + + /* Create the table if it doesn't exist */ + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + + N''') AND NOT EXISTS (SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.TABLES WHERE QUOTENAME(TABLE_SCHEMA) = ''' + + @OutputSchemaName + N''' AND QUOTENAME(TABLE_NAME) = ''' + + @OutputTableName + N''') CREATE TABLE ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'('; + SET @StringToExecute = @StringToExecute + N' + ID INT IDENTITY(1,1) NOT NULL, + ServerName NVARCHAR(128) NOT NULL, + CheckDate DATETIMEOFFSET NOT NULL, + [elapsed_time] [varchar](41) NULL, + [session_id] [smallint] NOT NULL, + [database_name] [nvarchar](128) NULL, + [query_text] [nvarchar](max) NULL, + [outer_command] NVARCHAR(4000) NULL, + [query_plan] [xml] NULL, + [live_query_plan] [xml] NULL, + [cached_parameter_info] [nvarchar](max) NULL, + [live_parameter_info] [nvarchar](max) NULL, + [query_cost] [float] NULL, + [status] [nvarchar](30) NOT NULL, + [wait_info] [nvarchar](max) NULL, + [wait_resource] [nvarchar](max) NULL, + [top_session_waits] [nvarchar](max) NULL, + [blocking_session_id] [smallint] NULL, + [open_transaction_count] [int] NULL, + [is_implicit_transaction] [int] NOT NULL, + [nt_domain] [nvarchar](128) NULL, + [host_name] [nvarchar](128) NULL, + [login_name] [nvarchar](128) NOT NULL, + [nt_user_name] [nvarchar](128) NULL, + [program_name] [nvarchar](128) NULL, + [fix_parameter_sniffing] [nvarchar](150) NULL, + [client_interface_name] [nvarchar](32) NULL, + [login_time] [datetime] NOT NULL, + [start_time] [datetime] NULL, + [request_time] [datetime] NULL, + [request_cpu_time] [int] NULL, + [request_logical_reads] [bigint] NULL, + [request_writes] [bigint] NULL, + [request_physical_reads] [bigint] NULL, + [session_cpu] [int] NOT NULL, + [session_logical_reads] [bigint] NOT NULL, + [session_physical_reads] [bigint] NOT NULL, + [session_writes] [bigint] NOT NULL, + [tempdb_allocations_mb] [decimal](38, 2) NULL, + [memory_usage] [int] NOT NULL, + [estimated_completion_time] [bigint] NULL, + [percent_complete] [real] NULL, + [deadlock_priority] [int] NULL, + [transaction_isolation_level] [varchar](33) NOT NULL, + [degree_of_parallelism] [smallint] NULL, + [last_dop] [bigint] NULL, + [min_dop] [bigint] NULL, + [max_dop] [bigint] NULL, + [last_grant_kb] [bigint] NULL, + [min_grant_kb] [bigint] NULL, + [max_grant_kb] [bigint] NULL, + [last_used_grant_kb] [bigint] NULL, + [min_used_grant_kb] [bigint] NULL, + [max_used_grant_kb] [bigint] NULL, + [last_ideal_grant_kb] [bigint] NULL, + [min_ideal_grant_kb] [bigint] NULL, + [max_ideal_grant_kb] [bigint] NULL, + [last_reserved_threads] [bigint] NULL, + [min_reserved_threads] [bigint] NULL, + [max_reserved_threads] [bigint] NULL, + [last_used_threads] [bigint] NULL, + [min_used_threads] [bigint] NULL, + [max_used_threads] [bigint] NULL, + [grant_time] [varchar](20) NULL, + [requested_memory_kb] [bigint] NULL, + [grant_memory_kb] [bigint] NULL, + [is_request_granted] [varchar](39) NOT NULL, + [required_memory_kb] [bigint] NULL, + [query_memory_grant_used_memory_kb] [bigint] NULL, + [ideal_memory_kb] [bigint] NULL, + [is_small] [bit] NULL, + [timeout_sec] [int] NULL, + [resource_semaphore_id] [smallint] NULL, + [wait_order] [varchar](20) NULL, + [wait_time_ms] [varchar](20) NULL, + [next_candidate_for_memory_grant] [varchar](3) NOT NULL, + [target_memory_kb] [bigint] NULL, + [max_target_memory_kb] [varchar](30) NULL, + [total_memory_kb] [bigint] NULL, + [available_memory_kb] [bigint] NULL, + [granted_memory_kb] [bigint] NULL, + [query_resource_semaphore_used_memory_kb] [bigint] NULL, + [grantee_count] [int] NULL, + [waiter_count] [int] NULL, + [timeout_error_count] [bigint] NULL, + [forced_grant_count] [varchar](30) NULL, + [workload_group_name] [sysname] NULL, + [resource_pool_name] [sysname] NULL, + [context_info] [varchar](128) NULL, + [query_hash] [binary](8) NULL, + [query_plan_hash] [binary](8) NULL, + [sql_handle] [varbinary] (64) NULL, + [plan_handle] [varbinary] (64) NULL, + [statement_start_offset] INT NULL, + [statement_end_offset] INT NULL, + JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50)), + PRIMARY KEY CLUSTERED (ID ASC));'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC(@StringToExecute); + + /* If the table doesn't have the new JoinKey computed column, add it. See Github #2162. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''JoinKey'') + ALTER TABLE ' + @ObjectFullName + N' ADD JoinKey AS ServerName + CAST(CheckDate AS NVARCHAR(50));'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new cached_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''cached_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD cached_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new live_parameter_info computed column, add it. See Github #2842. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''live_parameter_info'') + ALTER TABLE ' + @ObjectFullName + N' ADD live_parameter_info NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new outer_command column, add it. See Github #2887. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''outer_command'') + ALTER TABLE ' + @ObjectFullName + N' ADD outer_command NVARCHAR(4000) NULL;'; + EXEC(@StringToExecute); + + /* If the table doesn't have the new wait_resource column, add it. See Github #2970. */ + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableName; + SET @StringToExecute = N'IF NOT EXISTS (SELECT * FROM ' + @OutputDatabaseName + N'.sys.all_columns + WHERE object_id = (OBJECT_ID(''' + @ObjectFullName + N''')) AND name = ''wait_resource'') + ALTER TABLE ' + @ObjectFullName + N' ADD wait_resource NVARCHAR(MAX) NULL;'; + EXEC(@StringToExecute); + + /* Delete history older than @OutputTableRetentionDays */ + SET @OutputTableCleanupDate = CAST( (DATEADD(DAY, -1 * @OutputTableRetentionDays, GETDATE() ) ) AS DATE); + SET @StringToExecute = N' IF EXISTS(SELECT * FROM ' + + @OutputDatabaseName + + N'.INFORMATION_SCHEMA.SCHEMATA WHERE QUOTENAME(SCHEMA_NAME) = ''' + + @OutputSchemaName + N''') DELETE ' + + @OutputDatabaseName + '.' + + @OutputSchemaName + '.' + + @OutputTableName + + N' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + EXEC sp_executesql @StringToExecute, + N'@SrvName NVARCHAR(128), @CheckDate date', + @@SERVERNAME, @OutputTableCleanupDate; + + SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNameQueryStats_View; + + /* Create the view */ + IF OBJECT_ID(@ObjectFullName) IS NULL + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + + N'; EXEC (''CREATE VIEW ' + + @OutputSchemaName + '.' + + @OutputTableNameQueryStats_View + N' AS ' + @LineFeed + + N'WITH MaxQueryDuration AS ' + @LineFeed + + N'( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' MIN([ID]) AS [MinID], ' + @LineFeed + + N' MAX([ID]) AS [MaxID] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' GROUP BY [ServerName], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [sql_handle] ' + @LineFeed + + N') ' + @LineFeed + + N'SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @LineFeed + + N' ( ' + @LineFeed + + N' SELECT ' + @LineFeed + + N' [ID], ' + @LineFeed + + N' [ServerName], ' + @LineFeed + + N' [CheckDate], ' + @LineFeed + + N' [elapsed_time], ' + @LineFeed + + N' [session_id], ' + @LineFeed + + N' [database_name], ' + @LineFeed + + N' /* Truncate the query text to aid performance of painting the rows in SSMS */ ' + @LineFeed + + N' CAST([query_text] AS NVARCHAR(1000)) AS [query_text_snippet], ' + @LineFeed + + N' [query_plan], ' + @LineFeed + + N' [live_query_plan], ' + @LineFeed + + N' [query_cost], ' + @LineFeed + + N' [status], ' + @LineFeed + + N' [wait_info], ' + @LineFeed + + N' [wait_resource], ' + @LineFeed + + N' [top_session_waits], ' + @LineFeed + + N' [blocking_session_id], ' + @LineFeed + + N' [open_transaction_count], ' + @LineFeed + + N' [is_implicit_transaction], ' + @LineFeed + + N' [nt_domain], ' + @LineFeed + + N' [host_name], ' + @LineFeed + + N' [login_name], ' + @LineFeed + + N' [nt_user_name], ' + @LineFeed + + N' [program_name], ' + @LineFeed + + N' [fix_parameter_sniffing], ' + @LineFeed + + N' [client_interface_name], ' + @LineFeed + + N' [login_time], ' + @LineFeed + + N' [start_time], ' + @LineFeed + + N' [request_time], ' + @LineFeed + + N' [request_cpu_time], ' + @LineFeed + + N' [degree_of_parallelism], ' + @LineFeed + + N' [request_logical_reads], ' + @LineFeed + + N' ((CAST([request_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Reads_MB], ' + @LineFeed + + N' [request_writes], ' + @LineFeed + + N' ((CAST([request_writes] AS DECIMAL(38,2))* 8)/ 1024) [Logical_Writes_MB], ' + @LineFeed + + N' [request_physical_reads], ' + @LineFeed + + N' ((CAST([request_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [Physical_reads_MB], ' + @LineFeed + + N' [session_cpu], ' + @LineFeed + + N' [session_logical_reads], ' + @LineFeed + + N' ((CAST([session_logical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_logical_reads_MB], ' + @LineFeed + + N' [session_physical_reads], ' + @LineFeed + + N' ((CAST([session_physical_reads] AS DECIMAL(38,2))* 8)/ 1024) [session_physical_reads_MB], ' + @LineFeed + + N' [session_writes], ' + @LineFeed + + N' ((CAST([session_writes] AS DECIMAL(38,2))* 8)/ 1024) [session_writes_MB], ' + @LineFeed + + N' [tempdb_allocations_mb], ' + @LineFeed + + N' [memory_usage], ' + @LineFeed + + N' [estimated_completion_time], ' + @LineFeed + + N' [percent_complete], ' + @LineFeed + + N' [deadlock_priority], ' + @LineFeed + + N' [transaction_isolation_level], ' + @LineFeed + + N' [last_dop], ' + @LineFeed + + N' [min_dop], ' + @LineFeed + + N' [max_dop], ' + @LineFeed + + N' [last_grant_kb], ' + @LineFeed + + N' [min_grant_kb], ' + @LineFeed + + N' [max_grant_kb], ' + @LineFeed + + N' [last_used_grant_kb], ' + @LineFeed + + N' [min_used_grant_kb], ' + @LineFeed + + N' [max_used_grant_kb], ' + @LineFeed + + N' [last_ideal_grant_kb], ' + @LineFeed + + N' [min_ideal_grant_kb], ' + @LineFeed + + N' [max_ideal_grant_kb], ' + @LineFeed + + N' [last_reserved_threads], ' + @LineFeed + + N' [min_reserved_threads], ' + @LineFeed + + N' [max_reserved_threads], ' + @LineFeed + + N' [last_used_threads], ' + @LineFeed + + N' [min_used_threads], ' + @LineFeed + + N' [max_used_threads], ' + @LineFeed + + N' [grant_time], ' + @LineFeed + + N' [requested_memory_kb], ' + @LineFeed + + N' [grant_memory_kb], ' + @LineFeed + + N' [is_request_granted], ' + @LineFeed + + N' [required_memory_kb], ' + @LineFeed + + N' [query_memory_grant_used_memory_kb], ' + @LineFeed + + N' [ideal_memory_kb], ' + @LineFeed + + N' [is_small], ' + @LineFeed + + N' [timeout_sec], ' + @LineFeed + + N' [resource_semaphore_id], ' + @LineFeed + + N' [wait_order], ' + @LineFeed + + N' [wait_time_ms], ' + @LineFeed + + N' [next_candidate_for_memory_grant], ' + @LineFeed + + N' [target_memory_kb], ' + @LineFeed + + N' [max_target_memory_kb], ' + @LineFeed + + N' [total_memory_kb], ' + @LineFeed + + N' [available_memory_kb], ' + @LineFeed + + N' [granted_memory_kb], ' + @LineFeed + + N' [query_resource_semaphore_used_memory_kb], ' + @LineFeed + + N' [grantee_count], ' + @LineFeed + + N' [waiter_count], ' + @LineFeed + + N' [timeout_error_count], ' + @LineFeed + + N' [forced_grant_count], ' + @LineFeed + + N' [workload_group_name], ' + @LineFeed + + N' [resource_pool_name], ' + @LineFeed + + N' [context_info], ' + @LineFeed + + N' [query_hash], ' + @LineFeed + + N' [query_plan_hash], ' + @LineFeed + + N' [sql_handle], ' + @LineFeed + + N' [plan_handle], ' + @LineFeed + + N' [statement_start_offset], ' + @LineFeed + + N' [statement_end_offset] ' + @LineFeed + + N' FROM ' + @OutputSchemaName + '.' + @OutputTableName + '' + @LineFeed + + N' ) AS [BlitzWho] ' + @LineFeed + + N'INNER JOIN [MaxQueryDuration] ON [BlitzWho].[ID] = [MaxQueryDuration].[MaxID]; ' + @LineFeed + + N''');' + + IF @Debug = 1 + BEGIN + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) + END + + EXEC(@StringToExecute); + END; + + END + + IF OBJECT_ID('tempdb..#WhoReadableDBs') IS NOT NULL + DROP TABLE #WhoReadableDBs; + +CREATE TABLE #WhoReadableDBs +( +database_id INT +); + +IF EXISTS (SELECT * FROM sys.all_objects o WHERE o.name = 'dm_hadr_database_replica_states') BEGIN -SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - + RAISERROR('Checking for Read intent databases to exclude',0,0) WITH NOWAIT; + + EXEC('INSERT INTO #WhoReadableDBs (database_id) SELECT DBs.database_id FROM sys.databases DBs INNER JOIN sys.availability_replicas Replicas ON DBs.replica_id = Replicas.replica_id WHERE replica_server_name NOT IN (SELECT DISTINCT primary_replica FROM sys.dm_hadr_availability_group_states States) AND Replicas.secondary_role_allow_connections_desc = ''READ_ONLY'' AND replica_server_name = @@SERVERNAME;'); +END + +SELECT @BlockingCheck = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; + + SET LOCK_TIMEOUT 1000; /* To avoid blocking on live query plans. See Github issue #2907. */ DECLARE @blocked TABLE ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL + dbid SMALLINT NOT NULL, + last_batch DATETIME NOT NULL, + open_tran SMALLINT NOT NULL, + sql_handle BINARY(20) NOT NULL, + session_id SMALLINT NOT NULL, + blocking_session_id SMALLINT NOT NULL, + lastwaittype NCHAR(32) NOT NULL, + waittime BIGINT NOT NULL, + cpu INT NOT NULL, + physical_io BIGINT NOT NULL, + memusage INT NOT NULL ); INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) @@ -138,141 +617,256 @@ SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; FROM sys.sysprocesses AS sys1 JOIN sys.sysprocesses AS sys2 ON sys1.spid = sys2.blocked; + '+CASE + WHEN (@GetOuterCommand = 1 AND (NOT EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer'))) THEN N' + DECLARE @session_id SMALLINT; + DECLARE @Sessions TABLE + ( + session_id INT + ); + + DECLARE @inputbuffer TABLE + ( + ID INT IDENTITY(1,1), + session_id INT, + event_type NVARCHAR(30), + parameters SMALLINT, + event_info NVARCHAR(4000) + ); + + DECLARE inputbuffer_cursor + + CURSOR LOCAL FAST_FORWARD + FOR + SELECT session_id + FROM sys.dm_exec_sessions + WHERE session_id <> @@SPID + AND is_user_process = 1; + + OPEN inputbuffer_cursor; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id; + + WHILE (@@FETCH_STATUS = 0) + BEGIN; + BEGIN TRY; + + INSERT INTO @inputbuffer ([event_type],[parameters],[event_info]) + EXEC sp_executesql + N''DBCC INPUTBUFFER(@session_id) WITH NO_INFOMSGS;'', + N''@session_id SMALLINT'', + @session_id; + + UPDATE @inputbuffer + SET session_id = @session_id + WHERE ID = SCOPE_IDENTITY(); + + END TRY + BEGIN CATCH + RAISERROR(''DBCC inputbuffer failed for session %d'',0,0,@session_id) WITH NOWAIT; + END CATCH; + + FETCH NEXT FROM inputbuffer_cursor INTO @session_id + + END; + + CLOSE inputbuffer_cursor; + DEALLOCATE inputbuffer_cursor;' + ELSE N'' + END+ + N' + + DECLARE @LiveQueryPlans TABLE + ( + Session_Id INT NOT NULL, + Query_Plan XML NOT NULL + ); - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan , - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), - blocked.waittime) + '')'' ) AS wait_info , - CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id + ' +IF EXISTS (SELECT * FROM sys.all_columns WHERE object_id = OBJECT_ID('sys.dm_exec_query_statistics_xml') AND name = 'query_plan' AND @GetLiveQueryPlan=1) +BEGIN + SET @BlockingCheck = @BlockingCheck + N' + INSERT INTO @LiveQueryPlans + SELECT s.session_id, query_plan + FROM sys.dm_exec_sessions AS s + CROSS APPLY sys.dm_exec_query_statistics_xml(s.session_id) + WHERE s.session_id <> @@SPID;'; +END + + +IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 +BEGIN + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SET @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id ELSE NULL END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource , + COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , + IF @ExpertMode = 1 + BEGIN + SET @StringToExecute += + N', + ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , + s.memory_usage , + r.estimated_completion_time , r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , qmg.dop AS degree_of_parallelism , - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''N/A'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, wg.name AS workload_group_name , rp.name AS resource_pool_name, CONVERT(VARCHAR(128), r.context_info) AS context_info ' - END + END /* IF @ExpertMode = 1 */ - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle + SET @StringToExecute += + N'FROM sys.dm_exec_sessions AS s + '+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N'OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N'LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id LEFT JOIN sys.resource_governor_workload_groups wg ON s.group_id = wg.group_id LEFT JOIN sys.resource_governor_resource_pools rp @@ -286,277 +880,532 @@ SET @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; OR s.session_id = b.blocking_session_id) ) AS blocked OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb FROM sys.dm_db_task_space_usage tsu WHERE tsu.request_id = r.request_id AND tsu.session_id = r.session_id AND tsu.session_id = s.session_id ) as tempdb_allocations - WHERE s.session_id <> @@SPID + WHERE s.session_id <> @@SPID AND s.host_name IS NOT NULL ' + CASE WHEN @ShowSleepingSPIDs = 0 THEN N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' WHEN @ShowSleepingSPIDs = 1 THEN N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' -END + ELSE N'' END; +END /* IF @ProductVersionMajor > 9 and @ProductVersionMajor < 11 */ + IF @ProductVersionMajor >= 11 -BEGIN -SELECT @EnhanceFlag = - CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 - WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 - WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 - ELSE 0 - END + BEGIN + SELECT @EnhanceFlag = + CASE WHEN @ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020 THEN 1 + WHEN @ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 THEN 1 + WHEN @ProductVersionMajor = 13 AND @ProductVersionMinor >= 1601 THEN 1 + WHEN @ProductVersionMajor > 13 THEN 1 + ELSE 0 + END -IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL -BEGIN - SET @SessionWaits = 1 -END + IF OBJECT_ID('sys.dm_exec_session_wait_stats') IS NOT NULL + BEGIN + SET @SessionWaits = 1 + END + /* Think of the StringToExecute as starting with this, but we'll set this up later depending on whether we're doing an insert or a select: + SELECT @StringToExecute = N'SELECT GETDATE() AS run_date , + */ + SELECT @StringToExecute = N' CASE WHEN YEAR(s.last_request_start_time) = 1900 THEN NULL ELSE COALESCE( RIGHT(''00'' + CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), (DATEADD(SECOND, (r.total_elapsed_time / 1000), 0) + DATEADD(MILLISECOND, (r.total_elapsed_time % 1000), 0)), 114), RIGHT(''00'' + CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400), 2) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) ) END AS [elapsed_time] , + s.session_id , + CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL + THEN r.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id = 0 AND s.session_id = blocked.session_id + THEN blocked.blocking_session_id + WHEN r.blocking_session_id <> 0 AND s.session_id = blocked.blocking_session_id + THEN r.blocking_session_id + ELSE NULL + END AS blocking_session_id, + COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, + ISNULL(SUBSTRING(dest.text, + ( r.statement_start_offset / 2 ) + 1, + ( ( CASE r.statement_end_offset + WHEN -1 THEN DATALENGTH(dest.text) + ELSE r.statement_end_offset + END - r.statement_start_offset ) + / 2 ) + 1), dest.text) AS query_text , + '+CASE + WHEN @GetOuterCommand = 1 THEN N'CAST(event_info AS NVARCHAR(4000)) AS outer_command,' + ELSE N'' + END+N' + derp.query_plan , + CAST(COALESCE(qs_live.Query_Plan, ' + CASE WHEN @GetLiveQueryPlan=1 + THEN '''''' + ELSE '''''' + END + +') AS XML + + + ) AS live_query_plan , + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + FROM derp.query_plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Cached_Parameter_Info, + ' + IF @ShowActualParameters = 1 + BEGIN + SELECT @StringToExecute = @StringToExecute + N'qs_live.Live_Parameter_Info as Live_Parameter_Info,' + END + SELECT @StringToExecute = @StringToExecute + N' + qmg.query_cost , + s.status , + CASE + WHEN s.status <> ''sleeping'' THEN COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) + ELSE NULL + END AS wait_info , + r.wait_resource ,' + + + CASE @SessionWaits + WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' + ELSE N' NULL AS top_session_waits ,' + END + + + N'COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , + CASE WHEN EXISTS ( SELECT 1 + FROM sys.dm_tran_active_transactions AS tat + JOIN sys.dm_tran_session_transactions AS tst + ON tst.transaction_id = tat.transaction_id + WHERE tat.name = ''implicit_transaction'' + AND s.session_id = tst.session_id + ) THEN 1 + ELSE 0 + END AS is_implicit_transaction , + s.nt_domain , + s.host_name , + s.login_name , + s.nt_user_name ,' + IF @Platform = 'NonAzure' + BEGIN + SET @StringToExecute += + N'program_name = COALESCE(( + SELECT REPLACE(program_name,Substring(program_name,30,34),''"''+j.name+''"'') + FROM msdb.dbo.sysjobs j WHERE Substring(program_name,32,32) = CONVERT(char(32),CAST(j.job_id AS binary(16)),2) + ),s.program_name)' + END + ELSE + BEGIN + SET @StringToExecute += N's.program_name' + END -SELECT @StringToExecute = N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; - - DECLARE @blocked TABLE - ( - dbid SMALLINT NOT NULL, - last_batch DATETIME NOT NULL, - open_tran SMALLINT NOT NULL, - sql_handle BINARY(20) NOT NULL, - session_id SMALLINT NOT NULL, - blocking_session_id SMALLINT NOT NULL, - lastwaittype NCHAR(32) NOT NULL, - waittime BIGINT NOT NULL, - cpu INT NOT NULL, - physical_io BIGINT NOT NULL, - memusage INT NOT NULL - ); - - INSERT @blocked ( dbid, last_batch, open_tran, sql_handle, session_id, blocking_session_id, lastwaittype, waittime, cpu, physical_io, memusage ) - SELECT - sys1.dbid, sys1.last_batch, sys1.open_tran, sys1.sql_handle, - sys2.spid AS session_id, sys2.blocked AS blocking_session_id, sys2.lastwaittype, sys2.waittime, sys2.cpu, sys2.physical_io, sys2.memusage - FROM sys.sysprocesses AS sys1 - JOIN sys.sysprocesses AS sys2 - ON sys1.spid = sys2.blocked; + IF @ExpertMode = 1 /* We show more columns in expert mode, so the SELECT gets longer */ + BEGIN + SET @StringToExecute += + N', ''DBCC FREEPROCCACHE ('' + CONVERT(NVARCHAR(128), r.plan_handle, 1) + '');'' AS fix_parameter_sniffing, + s.client_interface_name , + s.login_time , + r.start_time , + qmg.request_time , + COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, + COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, + COALESCE(r.writes, s.writes) AS request_writes, + COALESCE(r.reads, s.reads) AS request_physical_reads , + s.cpu_time AS session_cpu, + s.logical_reads AS session_logical_reads, + s.reads AS session_physical_reads , + s.writes AS session_writes, + tempdb_allocations.tempdb_allocations_mb, + s.memory_usage , + r.estimated_completion_time , + r.percent_complete , + r.deadlock_priority , + CASE + WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' + WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' + WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.databases WHERE name = DB_NAME(r.database_id) AND is_read_committed_snapshot_on = 1) THEN ''Read Committed Snapshot Isolation'' + WHEN s.transaction_isolation_level = 2 THEN ''Read Committed'' + WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' + WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' + WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' + ELSE ''WHAT HAVE YOU DONE?'' + END AS transaction_isolation_level , + qmg.dop AS degree_of_parallelism , ' + + + CASE @EnhanceFlag + WHEN 1 THEN N'query_stats.last_dop, + query_stats.min_dop, + query_stats.max_dop, + query_stats.last_grant_kb, + query_stats.min_grant_kb, + query_stats.max_grant_kb, + query_stats.last_used_grant_kb, + query_stats.min_used_grant_kb, + query_stats.max_used_grant_kb, + query_stats.last_ideal_grant_kb, + query_stats.min_ideal_grant_kb, + query_stats.max_ideal_grant_kb, + query_stats.last_reserved_threads, + query_stats.min_reserved_threads, + query_stats.max_reserved_threads, + query_stats.last_used_threads, + query_stats.min_used_threads, + query_stats.max_used_threads,' + ELSE N' NULL AS last_dop, + NULL AS min_dop, + NULL AS max_dop, + NULL AS last_grant_kb, + NULL AS min_grant_kb, + NULL AS max_grant_kb, + NULL AS last_used_grant_kb, + NULL AS min_used_grant_kb, + NULL AS max_used_grant_kb, + NULL AS last_ideal_grant_kb, + NULL AS min_ideal_grant_kb, + NULL AS max_ideal_grant_kb, + NULL AS last_reserved_threads, + NULL AS min_reserved_threads, + NULL AS max_reserved_threads, + NULL AS last_used_threads, + NULL AS min_used_threads, + NULL AS max_used_threads,' + END - SELECT GETDATE() AS run_date , - COALESCE( - CONVERT(VARCHAR(20), (ABS(r.total_elapsed_time) / 1000) / 86400) + '':'' + CONVERT(VARCHAR(20), DATEADD(SECOND, (r.total_elapsed_time / 1000), 0), 114) , - CONVERT(VARCHAR(20), DATEDIFF(SECOND, s.last_request_start_time, GETDATE()) / 86400) + '':'' - + CONVERT(VARCHAR(20), DATEADD(SECOND, DATEDIFF(SECOND, s.last_request_start_time, GETDATE()), 0), 114) - ) AS [elapsed_time] , - s.session_id , - COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid), ''N/A'') AS database_name, - ISNULL(SUBSTRING(dest.text, - ( query_stats.statement_start_offset / 2 ) + 1, - ( ( CASE query_stats.statement_end_offset - WHEN -1 THEN DATALENGTH(dest.text) - ELSE query_stats.statement_end_offset - END - query_stats.statement_start_offset ) - / 2 ) + 1), dest.text) AS query_text , - derp.query_plan ,' - + - CASE @QueryStatsXML - WHEN 1 THEN + @QueryStatsXMLselect - ELSE N'' - END - +' - qmg.query_cost , - s.status , - COALESCE(wt.wait_info, RTRIM(blocked.lastwaittype) + '' ('' + CONVERT(VARCHAR(10), blocked.waittime) + '')'' ) AS wait_info ,' - + - CASE @SessionWaits - WHEN 1 THEN + N'SUBSTRING(wt2.session_wait_info, 0, LEN(wt2.session_wait_info) ) AS top_session_waits ,' - ELSE N'' - END - + - N'CASE WHEN r.blocking_session_id <> 0 AND blocked.session_id IS NULL - THEN r.blocking_session_id - WHEN r.blocking_session_id <> 0 AND s.session_id <> blocked.blocking_session_id - THEN blocked.blocking_session_id - ELSE NULL - END AS blocking_session_id, - COALESCE(r.open_transaction_count, blocked.open_tran) AS open_transaction_count , - s.nt_domain , - s.host_name , - s.login_name , - s.nt_user_name , - s.program_name - ' - IF @ExpertMode = 1 - BEGIN - SET @StringToExecute += - N', - s.client_interface_name , - s.login_time , - r.start_time , - qmg.request_time , - COALESCE(r.cpu_time, s.cpu_time) AS request_cpu_time, - COALESCE(r.logical_reads, s.logical_reads) AS request_logical_reads, - COALESCE(r.writes, s.writes) AS request_writes, - COALESCE(r.reads, s.reads) AS request_physical_reads , - s.cpu_time AS session_cpu, - s.logical_reads AS session_logical_reads, - s.reads AS session_physical_reads , - s.writes AS session_writes, - tempdb_allocations.tempdb_allocations_mb, - s.memory_usage , - r.estimated_completion_time , - r.percent_complete , - r.deadlock_priority , - CASE - WHEN s.transaction_isolation_level = 0 THEN ''Unspecified'' - WHEN s.transaction_isolation_level = 1 THEN ''Read Uncommitted'' - WHEN s.transaction_isolation_level = 2 AND EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed Snapshot Isolation'' - WHEN s.transaction_isolation_level = 2 AND NOT EXISTS (SELECT 1 FROM sys.dm_tran_active_snapshot_database_transactions AS trn WHERE s.session_id = trn.session_id AND is_snapshot = 0 ) THEN ''Read Committed'' - WHEN s.transaction_isolation_level = 3 THEN ''Repeatable Read'' - WHEN s.transaction_isolation_level = 4 THEN ''Serializable'' - WHEN s.transaction_isolation_level = 5 THEN ''Snapshot'' - ELSE ''WHAT HAVE YOU DONE?'' - END AS transaction_isolation_level , - qmg.dop AS degree_of_parallelism , ' - + - CASE @EnhanceFlag - WHEN 1 THEN @EnhanceSQL - ELSE N'' - END - + - N' - COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , - qmg.requested_memory_kb , - qmg.granted_memory_kb AS grant_memory_kb, - CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' - WHEN qmg.requested_memory_kb < qmg.granted_memory_kb - THEN ''Query Granted Less Than Query Requested'' - ELSE ''Memory Request Granted'' - END AS is_request_granted , - qmg.required_memory_kb , - qmg.used_memory_kb AS query_memory_grant_used_memory_kb, - qmg.ideal_memory_kb , - qmg.is_small , - qmg.timeout_sec , - qmg.resource_semaphore_id , - COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , - COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), - ''N/A'') AS wait_time_ms , - CASE qmg.is_next_candidate - WHEN 0 THEN ''No'' - WHEN 1 THEN ''Yes'' - ELSE ''N/A'' - END AS next_candidate_for_memory_grant , - qrs.target_memory_kb , - COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS max_target_memory_kb , - qrs.total_memory_kb , - qrs.available_memory_kb , - qrs.granted_memory_kb , - qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, - qrs.grantee_count , - qrs.waiter_count , - qrs.timeout_error_count , - COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), - ''Small Query Resource Semaphore'') AS forced_grant_count, - wg.name AS workload_group_name, - rp.name AS resource_pool_name, - CONVERT(VARCHAR(128), r.context_info) AS context_info - ' - END + SET @StringToExecute += + N' + COALESCE(CAST(qmg.grant_time AS VARCHAR(20)), ''Memory Not Granted'') AS grant_time , + qmg.requested_memory_kb , + qmg.granted_memory_kb AS grant_memory_kb, + CASE WHEN qmg.grant_time IS NULL THEN ''N/A'' + WHEN qmg.requested_memory_kb < qmg.granted_memory_kb + THEN ''Query Granted Less Than Query Requested'' + ELSE ''Memory Request Granted'' + END AS is_request_granted , + qmg.required_memory_kb , + qmg.used_memory_kb AS query_memory_grant_used_memory_kb, + qmg.ideal_memory_kb , + qmg.is_small , + qmg.timeout_sec , + qmg.resource_semaphore_id , + COALESCE(CAST(qmg.wait_order AS VARCHAR(20)), ''N/A'') AS wait_order , + COALESCE(CAST(qmg.wait_time_ms AS VARCHAR(20)), + ''N/A'') AS wait_time_ms , + CASE qmg.is_next_candidate + WHEN 0 THEN ''No'' + WHEN 1 THEN ''Yes'' + ELSE ''N/A'' + END AS next_candidate_for_memory_grant , + qrs.target_memory_kb , + COALESCE(CAST(qrs.max_target_memory_kb AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS max_target_memory_kb , + qrs.total_memory_kb , + qrs.available_memory_kb , + qrs.granted_memory_kb , + qrs.used_memory_kb AS query_resource_semaphore_used_memory_kb, + qrs.grantee_count , + qrs.waiter_count , + qrs.timeout_error_count , + COALESCE(CAST(qrs.forced_grant_count AS VARCHAR(20)), + ''Small Query Resource Semaphore'') AS forced_grant_count, + wg.name AS workload_group_name, + rp.name AS resource_pool_name, + CONVERT(VARCHAR(128), r.context_info) AS context_info, + r.query_hash, r.query_plan_hash, r.sql_handle, r.plan_handle, r.statement_start_offset, r.statement_end_offset ' + END /* IF @ExpertMode = 1 */ - SET @StringToExecute += - N'FROM sys.dm_exec_sessions AS s - LEFT JOIN sys.dm_exec_requests AS r - ON r.session_id = s.session_id - LEFT JOIN ( SELECT DISTINCT - wait.session_id , - ( SELECT waitwait.wait_type + N'' ('' - + CAST(SUM(waitwait.wait_duration_ms) AS NVARCHAR(128)) - + N'' ms) '' - FROM sys.dm_os_waiting_tasks AS waitwait - WHERE waitwait.session_id = wait.session_id - GROUP BY waitwait.wait_type - ORDER BY SUM(waitwait.wait_duration_ms) DESC - FOR - XML PATH('''') ) AS wait_info - FROM sys.dm_os_waiting_tasks AS wait ) AS wt - ON s.session_id = wt.session_id - LEFT JOIN sys.dm_exec_query_stats AS query_stats - ON r.sql_handle = query_stats.sql_handle - AND r.plan_handle = query_stats.plan_handle - AND r.statement_start_offset = query_stats.statement_start_offset - AND r.statement_end_offset = query_stats.statement_end_offset - ' - + - CASE @SessionWaits - WHEN 1 THEN @SessionWaitsSQL - ELSE N'' - END - + - ' - LEFT JOIN sys.dm_exec_query_memory_grants qmg - ON r.session_id = qmg.session_id - AND r.request_id = qmg.request_id - LEFT JOIN sys.dm_exec_query_resource_semaphores qrs - ON qmg.resource_semaphore_id = qrs.resource_semaphore_id - AND qmg.pool_id = qrs.pool_id - LEFT JOIN sys.resource_governor_workload_groups wg - ON s.group_id = wg.group_id - LEFT JOIN sys.resource_governor_resource_pools rp - ON wg.pool_id = rp.pool_id - OUTER APPLY ( - SELECT TOP 1 - b.dbid, b.last_batch, b.open_tran, b.sql_handle, - b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime - FROM @blocked b - WHERE (s.session_id = b.session_id - OR s.session_id = b.blocking_session_id) - ) AS blocked - OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest - OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp - OUTER APPLY ( - SELECT CONVERT(DECIMAL(38,2), SUM( (((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8) / 1024.)) ) AS tempdb_allocations_mb - FROM sys.dm_db_task_space_usage tsu - WHERE tsu.request_id = r.request_id - AND tsu.session_id = r.session_id - AND tsu.session_id = s.session_id - ) as tempdb_allocations - ' - + - CASE @QueryStatsXML - WHEN 1 THEN @QueryStatsXMLSQL - ELSE N'' - END - + - ' - WHERE s.session_id <> @@SPID - AND s.host_name IS NOT NULL - ' - + CASE WHEN @ShowSleepingSPIDs = 0 THEN - N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' - WHEN @ShowSleepingSPIDs = 1 THEN - N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' - ELSE N'' END - + - ' ORDER BY 2 DESC; - ' + SET @StringToExecute += + N' FROM sys.dm_exec_sessions AS s'+ + CASE + WHEN @GetOuterCommand = 1 THEN CASE + WHEN EXISTS(SELECT 1 FROM sys.all_objects WHERE [name] = N'dm_exec_input_buffer') THEN N' + OUTER APPLY sys.dm_exec_input_buffer (s.session_id, 0) AS ib' + ELSE N' + LEFT JOIN @inputbuffer ib ON s.session_id = ib.session_id' + END + ELSE N'' + END+N' + LEFT JOIN sys.dm_exec_requests AS r + ON r.session_id = s.session_id + LEFT JOIN ( SELECT DISTINCT + wait.session_id , + ( SELECT waitwait.wait_type + N'' ('' + + CAST(MAX(waitwait.wait_duration_ms) AS NVARCHAR(128)) + + N'' ms) '' + FROM sys.dm_os_waiting_tasks AS waitwait + WHERE waitwait.session_id = wait.session_id + GROUP BY waitwait.wait_type + ORDER BY SUM(waitwait.wait_duration_ms) DESC + FOR + XML PATH('''') ) AS wait_info + FROM sys.dm_os_waiting_tasks AS wait ) AS wt + ON s.session_id = wt.session_id + LEFT JOIN sys.dm_exec_query_stats AS query_stats + ON r.sql_handle = query_stats.sql_handle + AND r.plan_handle = query_stats.plan_handle + AND r.statement_start_offset = query_stats.statement_start_offset + AND r.statement_end_offset = query_stats.statement_end_offset + ' + + + CASE @SessionWaits + WHEN 1 THEN @SessionWaitsSQL + ELSE N'' + END + + + N' + LEFT JOIN sys.dm_exec_query_memory_grants qmg + ON r.session_id = qmg.session_id + AND r.request_id = qmg.request_id + LEFT JOIN sys.dm_exec_query_resource_semaphores qrs + ON qmg.resource_semaphore_id = qrs.resource_semaphore_id + AND qmg.pool_id = qrs.pool_id + LEFT JOIN sys.resource_governor_workload_groups wg + ON s.group_id = wg.group_id + LEFT JOIN sys.resource_governor_resource_pools rp + ON wg.pool_id = rp.pool_id + OUTER APPLY ( + SELECT TOP 1 + b.dbid, b.last_batch, b.open_tran, b.sql_handle, + b.session_id, b.blocking_session_id, b.lastwaittype, b.waittime + FROM @blocked b + WHERE (s.session_id = b.session_id + OR s.session_id = b.blocking_session_id) + ) AS blocked + OUTER APPLY sys.dm_exec_sql_text(COALESCE(r.sql_handle, blocked.sql_handle)) AS dest + OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) AS derp + OUTER APPLY ( + SELECT CONVERT(DECIMAL(38,2), SUM( ((((tsu.user_objects_alloc_page_count - user_objects_dealloc_page_count) + (tsu.internal_objects_alloc_page_count - internal_objects_dealloc_page_count)) * 8) / 1024.)) ) AS tempdb_allocations_mb + FROM sys.dm_db_task_space_usage tsu + WHERE tsu.request_id = r.request_id + AND tsu.session_id = r.session_id + AND tsu.session_id = s.session_id + ) as tempdb_allocations + + OUTER APPLY ( + SELECT TOP 1 Query_Plan, + STUFF((SELECT DISTINCT N'', '' + Node.Data.value(''(@Column)[1]'', ''NVARCHAR(4000)'') + N'' {'' + Node.Data.value(''(@ParameterDataType)[1]'', ''NVARCHAR(4000)'') + N''}: '' + Node.Data.value(''(@ParameterCompiledValue)[1]'', ''NVARCHAR(4000)'') + N'' (Actual: '' + Node.Data.value(''(@ParameterRuntimeValue)[1]'', ''NVARCHAR(4000)'') + N'')'' + FROM q.Query_Plan.nodes(''/*:ShowPlanXML/*:BatchSequence/*:Batch/*:Statements/*:StmtSimple/*:QueryPlan/*:ParameterList/*:ColumnReference'') AS Node(Data) + FOR XML PATH('''')), 1,2,'''') + AS Live_Parameter_Info + FROM @LiveQueryPlans q + WHERE (s.session_id = q.Session_Id) + + ) AS qs_live + + WHERE s.session_id <> @@SPID + AND s.host_name IS NOT NULL + AND r.database_id NOT IN (SELECT database_id FROM #WhoReadableDBs) + ' + + CASE WHEN @ShowSleepingSPIDs = 0 THEN + N' AND COALESCE(DB_NAME(r.database_id), DB_NAME(blocked.dbid)) IS NOT NULL' + WHEN @ShowSleepingSPIDs = 1 THEN + N' OR COALESCE(r.open_transaction_count, blocked.open_tran) >= 1' + ELSE N'' END; + + +END /* IF @ProductVersionMajor >= 11 */ + +IF (@MinElapsedSeconds + @MinCPUTime + @MinLogicalReads + @MinPhysicalReads + @MinWrites + @MinTempdbMB + @MinRequestedMemoryKB + @MinBlockingSeconds) > 0 + BEGIN + /* They're filtering for something, so set up a where clause that will let any (not all combined) of the min triggers work: */ + SET @StringToExecute += N' AND (1 = 0 '; + IF @MinElapsedSeconds > 0 + SET @StringToExecute += N' OR ABS(COALESCE(r.total_elapsed_time,0)) / 1000 >= ' + CAST(@MinElapsedSeconds AS NVARCHAR(20)); + IF @MinCPUTime > 0 + SET @StringToExecute += N' OR COALESCE(r.cpu_time, s.cpu_time,0) / 1000 >= ' + CAST(@MinCPUTime AS NVARCHAR(20)); + IF @MinLogicalReads > 0 + SET @StringToExecute += N' OR COALESCE(r.logical_reads, s.logical_reads,0) >= ' + CAST(@MinLogicalReads AS NVARCHAR(20)); + IF @MinPhysicalReads > 0 + SET @StringToExecute += N' OR COALESCE(s.reads,0) >= ' + CAST(@MinPhysicalReads AS NVARCHAR(20)); + IF @MinWrites > 0 + SET @StringToExecute += N' OR COALESCE(r.writes, s.writes,0) >= ' + CAST(@MinWrites AS NVARCHAR(20)); + IF @MinTempdbMB > 0 + SET @StringToExecute += N' OR COALESCE(tempdb_allocations.tempdb_allocations_mb,0) >= ' + CAST(@MinTempdbMB AS NVARCHAR(20)); + IF @MinRequestedMemoryKB > 0 + SET @StringToExecute += N' OR COALESCE(qmg.requested_memory_kb,0) >= ' + CAST(@MinRequestedMemoryKB AS NVARCHAR(20)); + /* Blocking is a little different - we're going to return ALL of the queries if we meet the blocking threshold. */ + IF @MinBlockingSeconds > 0 + SET @StringToExecute += N' OR (SELECT SUM(waittime / 1000) FROM @blocked) >= ' + CAST(@MinBlockingSeconds AS NVARCHAR(20)); + SET @StringToExecute += N' ) '; + END + +SET @StringToExecute += + N' ORDER BY ' + CASE WHEN @SortOrder = 'session_id' THEN '[session_id] DESC' + WHEN @SortOrder = 'query_cost' THEN '[query_cost] DESC' + WHEN @SortOrder = 'database_name' THEN '[database_name] ASC' + WHEN @SortOrder = 'open_transaction_count' THEN '[open_transaction_count] DESC' + WHEN @SortOrder = 'is_implicit_transaction' THEN '[is_implicit_transaction] DESC' + WHEN @SortOrder = 'login_name' THEN '[login_name] ASC' + WHEN @SortOrder = 'program_name' THEN '[program_name] ASC' + WHEN @SortOrder = 'client_interface_name' THEN '[client_interface_name] ASC' + WHEN @SortOrder = 'request_cpu_time' THEN 'COALESCE(r.cpu_time, s.cpu_time) DESC' + WHEN @SortOrder = 'request_logical_reads' THEN 'COALESCE(r.logical_reads, s.logical_reads) DESC' + WHEN @SortOrder = 'request_writes' THEN 'COALESCE(r.writes, s.writes) DESC' + WHEN @SortOrder = 'request_physical_reads' THEN 'COALESCE(r.reads, s.reads) DESC ' + WHEN @SortOrder = 'session_cpu' THEN 's.cpu_time DESC' + WHEN @SortOrder = 'session_logical_reads' THEN 's.logical_reads DESC' + WHEN @SortOrder = 'session_physical_reads' THEN 's.reads DESC' + WHEN @SortOrder = 'session_writes' THEN 's.writes DESC' + WHEN @SortOrder = 'tempdb_allocations_mb' THEN '[tempdb_allocations_mb] DESC' + WHEN @SortOrder = 'memory_usage' THEN '[memory_usage] DESC' + WHEN @SortOrder = 'deadlock_priority' THEN 'r.deadlock_priority DESC' + WHEN @SortOrder = 'transaction_isolation_level' THEN 'r.[transaction_isolation_level] DESC' + WHEN @SortOrder = 'requested_memory_kb' THEN '[requested_memory_kb] DESC' + WHEN @SortOrder = 'grant_memory_kb' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'grant' THEN 'qmg.granted_memory_kb DESC' + WHEN @SortOrder = 'query_memory_grant_used_memory_kb' THEN 'qmg.used_memory_kb DESC' + WHEN @SortOrder = 'ideal_memory_kb' THEN '[ideal_memory_kb] DESC' + WHEN @SortOrder = 'workload_group_name' THEN 'wg.name ASC' + WHEN @SortOrder = 'resource_pool_name' THEN 'rp.name ASC' + ELSE '[elapsed_time] DESC' + END + ' + '; + + +IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL + AND EXISTS ( SELECT * + FROM sys.databases + WHERE QUOTENAME([name]) = @OutputDatabaseName) + BEGIN + SET @StringToExecute = N'USE ' + + @OutputDatabaseName + N'; ' + + @BlockingCheck + + + ' INSERT INTO ' + + @OutputSchemaName + N'.' + + @OutputTableName + + N'(ServerName + ,CheckDate + ,[elapsed_time] + ,[session_id] + ,[blocking_session_id] + ,[database_name] + ,[query_text]' + + CASE WHEN @GetOuterCommand = 1 THEN N',[outer_command]' ELSE N'' END + N' + ,[query_plan]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[live_query_plan]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[cached_parameter_info]' ELSE N'' END + + CASE WHEN @ProductVersionMajor >= 11 AND @ShowActualParameters = 1 THEN N',[Live_Parameter_Info]' ELSE N'' END + N' + ,[query_cost] + ,[status] + ,[wait_info] + ,[wait_resource]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N',[top_session_waits]' ELSE N'' END + N' + ,[open_transaction_count] + ,[is_implicit_transaction] + ,[nt_domain] + ,[host_name] + ,[login_name] + ,[nt_user_name] + ,[program_name] + ,[fix_parameter_sniffing] + ,[client_interface_name] + ,[login_time] + ,[start_time] + ,[request_time] + ,[request_cpu_time] + ,[request_logical_reads] + ,[request_writes] + ,[request_physical_reads] + ,[session_cpu] + ,[session_logical_reads] + ,[session_physical_reads] + ,[session_writes] + ,[tempdb_allocations_mb] + ,[memory_usage] + ,[estimated_completion_time] + ,[percent_complete] + ,[deadlock_priority] + ,[transaction_isolation_level] + ,[degree_of_parallelism]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[last_dop] + ,[min_dop] + ,[max_dop] + ,[last_grant_kb] + ,[min_grant_kb] + ,[max_grant_kb] + ,[last_used_grant_kb] + ,[min_used_grant_kb] + ,[max_used_grant_kb] + ,[last_ideal_grant_kb] + ,[min_ideal_grant_kb] + ,[max_ideal_grant_kb] + ,[last_reserved_threads] + ,[min_reserved_threads] + ,[max_reserved_threads] + ,[last_used_threads] + ,[min_used_threads] + ,[max_used_threads]' ELSE N'' END + N' + ,[grant_time] + ,[requested_memory_kb] + ,[grant_memory_kb] + ,[is_request_granted] + ,[required_memory_kb] + ,[query_memory_grant_used_memory_kb] + ,[ideal_memory_kb] + ,[is_small] + ,[timeout_sec] + ,[resource_semaphore_id] + ,[wait_order] + ,[wait_time_ms] + ,[next_candidate_for_memory_grant] + ,[target_memory_kb] + ,[max_target_memory_kb] + ,[total_memory_kb] + ,[available_memory_kb] + ,[granted_memory_kb] + ,[query_resource_semaphore_used_memory_kb] + ,[grantee_count] + ,[waiter_count] + ,[timeout_error_count] + ,[forced_grant_count] + ,[workload_group_name] + ,[resource_pool_name] + ,[context_info]' + + CASE WHEN @ProductVersionMajor >= 11 THEN N' + ,[query_hash] + ,[query_plan_hash] + ,[sql_handle] + ,[plan_handle] + ,[statement_start_offset] + ,[statement_end_offset]' ELSE N'' END + N' +) + SELECT @@SERVERNAME, COALESCE(@CheckDateOverride, SYSDATETIMEOFFSET()) AS CheckDate , ' + + @StringToExecute; + END +ELSE + SET @StringToExecute = @BlockingCheck + N' SELECT GETDATE() AS run_date , ' + @StringToExecute; + +/* If the server has > 50GB of memory, add a max grant hint to avoid getting a giant grant */ +IF (@ProductVersionMajor = 11 AND @ProductVersionMinor >= 6020) + OR (@ProductVersionMajor = 12 AND @ProductVersionMinor >= 5000 ) + OR (@ProductVersionMajor >= 13 ) + AND 50000000 < (SELECT cntr_value + FROM sys.dm_os_performance_counters + WHERE object_name LIKE '%:Memory Manager%' + AND counter_name LIKE 'Target Server Memory (KB)%') + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (MAX_GRANT_PERCENT = 1, RECOMPILE) '; + END +ELSE + BEGIN + SET @StringToExecute = @StringToExecute + N' OPTION (RECOMPILE) '; + END + +/* Be good: */ +SET @StringToExecute = @StringToExecute + N' ; '; -END IF @Debug = 1 BEGIN PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) - PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 160000)) + PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) END -EXEC(@StringToExecute); +EXEC sp_executesql @StringToExecute, + N'@CheckDateOverride DATETIMEOFFSET', + @CheckDateOverride; END -GO \ No newline at end of file +GO diff --git a/sp_DatabaseRestore.sql b/sp_DatabaseRestore.sql old mode 100644 new mode 100755 index d1fb9e014..1a087860f --- a/sp_DatabaseRestore.sql +++ b/sp_DatabaseRestore.sql @@ -1,183 +1,228 @@ +IF OBJECT_ID('dbo.CommandExecute') IS NULL +BEGIN + PRINT 'sp_DatabaseRestore is about to install, but you have not yet installed the Ola Hallengren maintenance scripts.' + PRINT 'sp_DatabaseRestore will still install, but to use that stored proc, you will need to install this:' + PRINT 'https://ola.hallengren.com' + PRINT 'You will see a bunch of warnings below because the Ola scripts are not installed yet, and that is okay:' +END +GO + + IF OBJECT_ID('dbo.sp_DatabaseRestore') IS NULL EXEC ('CREATE PROCEDURE dbo.sp_DatabaseRestore AS RETURN 0;'); GO - ALTER PROCEDURE [dbo].[sp_DatabaseRestore] - @Database NVARCHAR(128) = NULL, - @RestoreDatabaseName NVARCHAR(128) = NULL, - @BackupPathFull NVARCHAR(MAX) = NULL, - @BackupPathDiff NVARCHAR(MAX) = NULL, - @BackupPathLog NVARCHAR(MAX) = NULL, - @MoveFiles BIT = 0, - @MoveDataDrive NVARCHAR(260) = NULL, - @MoveLogDrive NVARCHAR(260) = NULL, - @TestRestore BIT = 0, - @RunCheckDB BIT = 0, - @RestoreDiff BIT = 0, - @ContinueLogs BIT = 0, - @StandbyMode BIT = 0, - @StandbyUndoPath NVARCHAR(MAX) = NULL, - @RunRecovery BIT = 0, - @ForceSimpleRecovery BIT = 0, - @StopAt NVARCHAR(14) = NULL, - @OnlyLogsAfter NVARCHAR(14) = NULL, - @Debug INT = 0, - @Help BIT = 0, - @VersionDate DATETIME = NULL OUTPUT + @Database NVARCHAR(128) = NULL, + @RestoreDatabaseName NVARCHAR(128) = NULL, + @BackupPathFull NVARCHAR(260) = NULL, + @BackupPathDiff NVARCHAR(260) = NULL, + @BackupPathLog NVARCHAR(260) = NULL, + @MoveFiles BIT = 1, + @MoveDataDrive NVARCHAR(260) = NULL, + @MoveLogDrive NVARCHAR(260) = NULL, + @MoveFilestreamDrive NVARCHAR(260) = NULL, + @MoveFullTextCatalogDrive NVARCHAR(260) = NULL, + @BufferCount INT = NULL, + @MaxTransferSize INT = NULL, + @BlockSize INT = NULL, + @TestRestore BIT = 0, + @RunCheckDB BIT = 0, + @RestoreDiff BIT = 0, + @ContinueLogs BIT = 0, + @StandbyMode BIT = 0, + @StandbyUndoPath NVARCHAR(MAX) = NULL, + @RunRecovery BIT = 0, + @ForceSimpleRecovery BIT = 0, + @ExistingDBAction TINYINT = 0, + @StopAt NVARCHAR(14) = NULL, + @OnlyLogsAfter NVARCHAR(14) = NULL, + @SimpleFolderEnumeration BIT = 0, + @SkipBackupsAlreadyInMsdb BIT = 0, + @DatabaseOwner sysname = NULL, + @SetTrustworthyON BIT = 0, + @FixOrphanUsers BIT = 0, + @KeepCdc BIT = 0, + @Execute CHAR(1) = Y, + @FileExtensionDiff NVARCHAR(128) = NULL, + @Debug INT = 0, + @Help BIT = 0, + @Version VARCHAR(30) = NULL OUTPUT, + @VersionDate DATETIME = NULL OUTPUT, + @VersionCheckMode BIT = 0, + @FileNamePrefix NVARCHAR(260) = NULL, + @RunStoredProcAfterRestore NVARCHAR(260) = NULL, + @EnableBroker BIT = 0 AS SET NOCOUNT ON; +SET STATISTICS XML OFF; /*Versioning details*/ - DECLARE @Version NVARCHAR(30); - SET @Version = '6.0'; - SET @VersionDate = '20171201'; +SELECT @Version = '8.26', @VersionDate = '20251002'; -IF @Help = 1 - - BEGIN - - PRINT ' - /* - sp_DatabaseRestore from http://FirstResponderKit.org - - This script will restore a database from a given file path. - - 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. - - Known limitations of this version: - - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. - - Tastes awful with marmite. - - Unknown limitations of this version: - - None. (If we knew them, they would be known. Duh.) - - Changes - for the full list of improvements and fixes in this version, see: - https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ - - MIT License - - Copyright for portions of sp_Blitz are held by Microsoft as part of project - tigertoolbox and are provided under the MIT license: - https://github.com/Microsoft/tigertoolbox - - All other copyright for sp_Blitz are held by Brent Ozar Unlimited, 2017. - - Copyright (c) 2017 Brent Ozar Unlimited - - 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. - - */ - '; - - PRINT ' - /* - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 0, - @RunRecovery = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 1, - @RunRecovery = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 1, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @ContinueLogs = 0, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', - @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', - @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1, - @TestRestore = 1, - @RunCheckDB = 1, - @Debug = 0; - - EXEC dbo.sp_DatabaseRestore - @Database = ''LogShipMe'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @StandbyMode = 1, - @StandbyUndoPath = ''D:\Data\'', - @ContinueLogs = 1, - @RunRecovery = 0, - @Debug = 0; - - --This example will restore the latest differential backup, and stop transaction logs at the specified date time. It will also only print the commands. - EXEC dbo.sp_DatabaseRestore - @Database = ''DBA'', - @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', - @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', - @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', - @RestoreDiff = 1, - @ContinueLogs = 0, - @RunRecovery = 1, - @StopAt = ''20170508201501'', - @Debug = 1; - - Variables: - - @RestoreDiff - This variable is a flag for whether or not the script is expecting to restore differentials - @StopAt - This variable is used to restore transaction logs to a specific date and time. The format must be in YYYYMMDDHHMMSS. The time is in military format. - - About Debug Modes: - - There are 3 Debug Modes. Mode 0 is the default and will execute the script. Debug 1 will print just the commands. Debug 2 will print other useful information that - has mostly been useful for troubleshooting. Debug 2 needs to be expanded to make it more useful. - */ - '; - +IF(@VersionCheckMode = 1) +BEGIN RETURN; - - END; +END; +IF @Help = 1 +BEGIN + PRINT ' + /* + sp_DatabaseRestore from http://FirstResponderKit.org + + This script will restore a database from a given file path. + + 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. + + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Tastes awful with marmite. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) + + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + + MIT License + + Copyright (c) Brent Ozar Unlimited + + 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. + + */ + '; + + PRINT ' + /* + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 0, + @RunRecovery = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 1, + @RunRecovery = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 1, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @ContinueLogs = 0, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', + @BackupPathLog = ''D:\Backup\SQL2016PROD1A\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @TestRestore = 1, + @RunCheckDB = 1, + @Debug = 0; + + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @StandbyMode = 1, + @StandbyUndoPath = ''D:\Data\'', + @ContinueLogs = 1, + @RunRecovery = 0, + @Debug = 0; + + --Restore just through the latest DIFF, ignoring logs, and using a custom ".dif" file extension + EXEC dbo.sp_DatabaseRestore + @Database = ''LogShipMe'', + @BackupPathFull = ''D:\Backup\SQL2016PROD1A\LogShipMe\FULL\'', + @BackupPathDiff = ''D:\Backup\SQL2016PROD1A\LogShipMe\DIFF\'', + @RestoreDiff = 1, + @FileExtensionDiff = ''dif'', + @ContinueLogs = 0, + @RunRecovery = 1; + + -- Restore from stripped backup set when multiple paths are used. This example will restore stripped full backup set along with stripped transactional logs set from multiple backup paths + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''D:\Backup1\DBA\FULL,D:\Backup2\DBA\FULL'', + @BackupPathLog = ''D:\Backup1\DBA\LOG,D:\Backup2\DBA\LOG'', + @StandbyMode = 0, + @ContinueLogs = 1, + @RunRecovery = 0, + @Debug = 0; + + --This example will restore the latest differential backup, and stop transaction logs at the specified date time. It will execute and print debug information. + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @StopAt = ''20170508201501'', + @Debug = 1; + + --This example will NOT execute the restore. Commands will be printed in a copy/paste ready format only + EXEC dbo.sp_DatabaseRestore + @Database = ''DBA'', + @BackupPathFull = ''\\StorageServer\LogShipMe\FULL\'', + @BackupPathDiff = ''\\StorageServer\LogShipMe\DIFF\'', + @BackupPathLog = ''\\StorageServer\LogShipMe\LOG\'', + @RestoreDiff = 1, + @ContinueLogs = 0, + @RunRecovery = 1, + @TestRestore = 1, + @RunCheckDB = 1, + @Debug = 0, + @Execute = ''N''; + '; + + RETURN; +END; -- Get the SQL Server version number because the columns returned by RESTORE commands vary by version -- Based on: https://www.brentozar.com/archive/2015/05/sql-server-version-detection/ @@ -189,10 +234,24 @@ DECLARE @BuildVersion AS SMALLINT = CAST(PARSENAME(@ProductVersion, 2) AS SMALLI IF @MajorVersion < 10 BEGIN - RAISERROR('Sorry, DatabaseRestore doesn''t work on versions of SQL prior to 2008.', 15, 1); - RETURN; + RAISERROR('Sorry, DatabaseRestore doesn''t work on versions of SQL prior to 2008.', 15, 1); + RETURN; END; +BEGIN TRY +DECLARE @CurrentDatabaseContext AS VARCHAR(128) = (SELECT DB_NAME()); +DECLARE @CommandExecuteCheck VARCHAR(400); + +SET @CommandExecuteCheck = 'IF NOT EXISTS (SELECT name FROM ' +@CurrentDatabaseContext+'.sys.objects WHERE type = ''P'' AND name = ''CommandExecute'') +BEGIN + RAISERROR (''DatabaseRestore requires the CommandExecute stored procedure from the OLA Hallengren Maintenance solution, are you using the correct database?'', 15, 1); + RETURN; +END;' +EXEC (@CommandExecuteCheck) +END TRY +BEGIN CATCH +THROW; +END CATCH DECLARE @cmd NVARCHAR(4000) = N'', --Holds xp_cmdshell command @sql NVARCHAR(MAX) = N'', --Holds executable SQL commands @@ -208,13 +267,28 @@ DECLARE @cmd NVARCHAR(4000) = N'', --Holds xp_cmdshell command @LogRecoveryOption AS NVARCHAR(MAX) = N'', --Holds the option to cause logs to be restored in standby mode or with no recovery @DatabaseLastLSN NUMERIC(25, 0), --redo_start_lsn of the current database @i TINYINT = 1, --Maintains loop to continue logs + @LogRestoreRanking INT = 1, --Holds Log iteration # when multiple paths & backup files are being stripped @LogFirstLSN NUMERIC(25, 0), --Holds first LSN in log backup headers @LogLastLSN NUMERIC(25, 0), --Holds last LSN in log backup headers - @FileListParamSQL NVARCHAR(4000) = N''; --Holds INSERT list for #FileListParameters + @LogLastNameInMsdbAS NVARCHAR(MAX) = N'', -- Holds last TRN file name already restored + @FileListParamSQL NVARCHAR(4000) = N'', --Holds INSERT list for #FileListParameters + @BackupParameters NVARCHAR(500) = N'', --Used to save BlockSize, MaxTransferSize and BufferCount + @RestoreDatabaseID SMALLINT, --Holds DB_ID of @RestoreDatabaseName + @UnquotedRestoreDatabaseName NVARCHAR(128); --Holds the unquoted @RestoreDatabaseName + +DECLARE @FileListSimple TABLE ( + BackupFile NVARCHAR(255) NOT NULL, + depth INT NOT NULL, + [file] INT NOT NULL +); -DECLARE @FileList TABLE -( - BackupFile NVARCHAR(255) +DECLARE @FileList TABLE ( + BackupPath NVARCHAR(255) NULL, + BackupFile NVARCHAR(255) NULL +); + +DECLARE @PathItem TABLE ( + PathItem NVARCHAR(512) ); @@ -223,7 +297,7 @@ CREATE TABLE #FileListParameters ( LogicalName NVARCHAR(128) NOT NULL, PhysicalName NVARCHAR(260) NOT NULL, - Type CHAR(1) NOT NULL, + [Type] CHAR(1) NOT NULL, FileGroupName NVARCHAR(120) NULL, Size NUMERIC(20, 0) NOT NULL, MaxSize NUMERIC(20, 0) NOT NULL, @@ -245,7 +319,6 @@ CREATE TABLE #FileListParameters SnapshotUrl NVARCHAR(360) NULL ); - IF OBJECT_ID(N'tempdb..#Headers') IS NOT NULL DROP TABLE #Headers; CREATE TABLE #Headers ( @@ -269,7 +342,7 @@ CREATE TABLE #Headers BackupStartDate NVARCHAR(256), BackupFinishDate NVARCHAR(256), SortOrder NVARCHAR(256), - CodePage NVARCHAR(256), + [CodePage] NVARCHAR(256), UnicodeLocaleId NVARCHAR(256), UnicodeComparisonStyle NVARCHAR(256), CompatibilityLevel NVARCHAR(256), @@ -305,6 +378,9 @@ CREATE TABLE #Headers KeyAlgorithm NVARCHAR(32), EncryptorThumbprint VARBINARY(20), EncryptorType NVARCHAR(32), + LastValidRestoreTime DATETIME, + TimeZone NVARCHAR(32), + CompressionAlgorithm NVARCHAR(32), -- -- Seq added to retain order by -- @@ -312,557 +388,958 @@ CREATE TABLE #Headers ); /* - -Correct paths in case people forget a final "\" - +Correct paths in case people forget a final "\" or "/" */ - /*Full*/ - -IF (SELECT RIGHT(@BackupPathFull, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @BackupPathFull to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathFull += N'\'; - END; - +IF (SELECT RIGHT(@BackupPathFull, 1)) <> '/' AND CHARINDEX('/', @BackupPathFull) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathFull to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathFull += N'/'; +END; +ELSE IF (SELECT RIGHT(@BackupPathFull, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathFull to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathFull += N'\'; +END; /*Diff*/ -IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @BackupPathDiff to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathDiff += N'\'; - END; - +IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '/' AND CHARINDEX('/', @BackupPathDiff) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathDiff to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathDiff += N'/'; +END; +ELSE IF (SELECT RIGHT(@BackupPathDiff, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathDiff to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathDiff += N'\'; +END; /*Log*/ +IF (SELECT RIGHT(@BackupPathLog, 1)) <> '/' AND CHARINDEX('/', @BackupPathLog) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathLog to add a "/"', 0, 1) WITH NOWAIT; + SET @BackupPathLog += N'/'; +END; IF (SELECT RIGHT(@BackupPathLog, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @BackupPathLog to add a "\"', 0, 1) WITH NOWAIT; - SET @BackupPathLog += N'\'; - END; - +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @BackupPathLog to add a "\"', 0, 1) WITH NOWAIT; + SET @BackupPathLog += N'\'; +END; /*Move Data File*/ IF NULLIF(@MoveDataDrive, '') IS NULL - BEGIN - RAISERROR('Getting default data drive for @MoveDataDrive', 0, 1) WITH NOWAIT; - SET @MoveDataDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); - END; -IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; - SET @MoveDataDrive += N'\'; - END; - +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Getting default data drive for @MoveDataDrive', 0, 1) WITH NOWAIT; + SET @MoveDataDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '/' AND CHARINDEX('/', @MoveDataDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveDataDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveDataDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveDataDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveDataDrive += N'\'; +END; /*Move Log File*/ IF NULLIF(@MoveLogDrive, '') IS NULL - BEGIN - RAISERROR('Getting default log drive for @@MoveLogDrive', 0, 1) WITH NOWAIT; - SET @MoveLogDrive = CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(260)); - END; -IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @MoveDataDrive to add a "\"', 0, 1) WITH NOWAIT; - SET @MoveLogDrive += N'\'; - END; - +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Getting default log drive for @MoveLogDrive', 0, 1) WITH NOWAIT; + SET @MoveLogDrive = CAST(SERVERPROPERTY('InstanceDefaultLogPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '/' AND CHARINDEX('/', @MoveLogDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing@MoveLogDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveLogDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveLogDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveLogDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveLogDrive += N'\'; +END; +/*Move Filestream File*/ +IF NULLIF(@MoveFilestreamDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Setting default data drive for @MoveFilestreamDrive', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveFilestreamDrive, 1)) <> '/' AND CHARINDEX('/', @MoveFilestreamDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFilestreamDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive += N'/'; +END; +ELSE IF (SELECT RIGHT(@MoveFilestreamDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFilestreamDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveFilestreamDrive += N'\'; +END; +/*Move FullText Catalog File*/ +IF NULLIF(@MoveFullTextCatalogDrive, '') IS NULL +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Setting default data drive for @MoveFullTextCatalogDrive', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive = CAST(SERVERPROPERTY('InstanceDefaultDataPath') AS nvarchar(260)); +END; +IF (SELECT RIGHT(@MoveFullTextCatalogDrive, 1)) <> '/' AND CHARINDEX('/', @MoveFullTextCatalogDrive) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFullTextCatalogDrive to add a "/"', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive += N'/'; +END; +IF (SELECT RIGHT(@MoveFullTextCatalogDrive, 1)) <> '\' --Has to end in a '\' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveFullTextCatalogDrive to add a "\"', 0, 1) WITH NOWAIT; + SET @MoveFullTextCatalogDrive += N'\'; +END; /*Standby Undo File*/ +IF (SELECT RIGHT(@StandbyUndoPath, 1)) <> '/' AND CHARINDEX('/', @StandbyUndoPath) > 0 --Has to end in a '/' +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @StandbyUndoPath to add a "/"', 0, 1) WITH NOWAIT; + SET @StandbyUndoPath += N'/'; +END; IF (SELECT RIGHT(@StandbyUndoPath, 1)) <> '\' --Has to end in a '\' - BEGIN - RAISERROR('Fixing @StandbyUndoPath to add a "\"', 0, 1) WITH NOWAIT; - SET @StandbyUndoPath += N'\'; - END; +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @StandbyUndoPath to add a "\"', 0, 1) WITH NOWAIT; + SET @StandbyUndoPath += N'\'; +END; +IF @RestoreDatabaseName IS NULL OR @RestoreDatabaseName LIKE N'' /*use LIKE instead of =, otherwise N'' = N' '. See: https://www.brentozar.com/archive/2017/04/surprising-behavior-trailing-spaces/ */ +BEGIN + SET @RestoreDatabaseName = @Database; +END; -IF @RestoreDatabaseName IS NULL +/*check input parameters*/ +IF NOT @MaxTransferSize IS NULL +BEGIN + IF @MaxTransferSize > 4194304 BEGIN - SET @RestoreDatabaseName = QUOTENAME(@Database); + RAISERROR('@MaxTransferSize can not be greater then 4194304', 0, 1) WITH NOWAIT; END -ELSE + + IF @MaxTransferSize % 64 <> 0 BEGIN - SET @RestoreDatabaseName = QUOTENAME(@RestoreDatabaseName) + RAISERROR('@MaxTransferSize has to be a multiple of 65536', 0, 1) WITH NOWAIT; END +END; +IF NOT @BlockSize IS NULL +BEGIN + IF @BlockSize NOT IN (512, 1024, 2048, 4096, 8192, 16384, 32768, 65536) + BEGIN + RAISERROR('Supported values for @BlockSize are 512, 1024, 2048, 4096, 8192, 16384, 32768, and 65536', 0, 1) WITH NOWAIT; + END +END -IF @BackupPathFull IS NOT NULL - +--File Extension cleanup +IF @FileExtensionDiff LIKE '%.%' BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Removing "." from @FileExtensionDiff', 0, 1) WITH NOWAIT; + SET @FileExtensionDiff = REPLACE(@FileExtensionDiff,'.',''); +END --- Get list of files -SET @cmd = N'DIR /b "' + @BackupPathFull + N'"'; +SET @RestoreDatabaseID = DB_ID(@RestoreDatabaseName); +SET @RestoreDatabaseName = QUOTENAME(@RestoreDatabaseName); +SET @UnquotedRestoreDatabaseName = PARSENAME(@RestoreDatabaseName,1); - IF @Debug = 1 - BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathFull'; - PRINT @cmd; - END; +--If xp_cmdshell is disabled, force use of xp_dirtree +IF NOT EXISTS (SELECT * FROM sys.configurations WHERE name = 'xp_cmdshell' AND value_in_use = 1) + SET @SimpleFolderEnumeration = 1; +SET @HeadersSQL = +N'INSERT INTO #Headers WITH (TABLOCK) + (BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, Position, DeviceType, UserName, ServerName + ,DatabaseName, DatabaseVersion, DatabaseCreationDate, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN + ,BackupStartDate, BackupFinishDate, SortOrder, CodePage, UnicodeLocaleId, UnicodeComparisonStyle, CompatibilityLevel + ,SoftwareVendorId, SoftwareVersionMajor, SoftwareVersionMinor, SoftwareVersionBuild, MachineName, Flags, BindingID + ,RecoveryForkID, Collation, FamilyGUID, HasBulkLoggedData, IsSnapshot, IsReadOnly, IsSingleUser, HasBackupChecksums + ,IsDamaged, BeginsLogChain, HasIncompleteMetaData, IsForceOffline, IsCopyOnly, FirstRecoveryForkID, ForkPointLSN + ,RecoveryModel, DifferentialBaseLSN, DifferentialBaseGUID, BackupTypeDescription, BackupSetGUID, CompressedBackupSize'; -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; +IF @MajorVersion >= 11 + SET @HeadersSQL += NCHAR(13) + NCHAR(10) + N', Containment'; -/*Sanity check folders*/ +IF @MajorVersion >= 13 OR (@MajorVersion = 12 AND @BuildVersion >= 2342) + SET @HeadersSQL += N', KeyAlgorithm, EncryptorThumbprint, EncryptorType'; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 +IF @MajorVersion >= 16 + SET @HeadersSQL += N', LastValidRestoreTime, TimeZone, CompressionAlgorithm'; - BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; +SET @HeadersSQL += N')' + NCHAR(13) + NCHAR(10); +SET @HeadersSQL += N'EXEC (''RESTORE HEADERONLY FROM DISK=''''{Path}'''''')'; - END; +IF @BackupPathFull IS NOT NULL +BEGIN + DECLARE @CurrentBackupPathFull NVARCHAR(255); + + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathFull, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathFull IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, EndPosition + 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathFull, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathFull, EndPosition + 1 ), 0 ), LEN( @BackupPathFull ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathFull ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 + BEGIN - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 + SELECT TOP 1 @CurrentBackupPathFull = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathFull, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @CurrentBackupPathFull, 1, 1; + INSERT @FileList (BackupPath,BackupFile) SELECT @CurrentBackupPathFull, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathFull) WITH NOWAIT; - + SET @cmd = N'DIR /b "' + @CurrentBackupPathFull + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathFull'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathFull + WHERE BackupPath IS NULL; END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 - + IF @Debug = 1 BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - + SELECT BackupPath, BackupFile FROM @FileList; + END; + IF @SimpleFolderEnumeration = 1 + BEGIN + /*Check what we can*/ + IF NOT EXISTS (SELECT * FROM @FileList) + BEGIN + RAISERROR('(FULL) No rows were returned for that database in path %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; END - - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The user name or password is incorrect.' - ) = 1 - + ELSE BEGIN - - RAISERROR('Incorrect user name or password for %s', 16, 1, @BackupPathFull) WITH NOWAIT; - + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + OR fl.BackupFile = 'File Not Found' + ) = 1 + BEGIN + RAISERROR('(FULL) No rows or bad value for path %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(FULL) Access is denied to %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + ) = 1 + AND + ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile IS NULL + ) = 1 + BEGIN + RAISERROR('(FULL) Empty directory %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(FULL) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathFull) WITH NOWAIT; + RETURN; + END; END; + END + /*End folder sanity check*/ -/*End folder sanity check*/ + IF @StopAt IS NOT NULL + BEGIN + DELETE + FROM @FileList + WHERE BackupFile LIKE N'%[_][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); --- Find latest full backup -SELECT @LastFullBackup = MAX(BackupFile) -FROM @FileList -WHERE BackupFile LIKE N'%.bak' - AND - BackupFile LIKE N'%' + @Database + N'%' - AND - (@StopAt IS NULL OR REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt); + DELETE + FROM @FileList + 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); + END; - IF @Debug = 1 + -- Find latest full backup + -- Get the TOP record to use in "Restore HeaderOnly/FileListOnly" statement as well as Non-Split Backups Restore Command + SELECT TOP 1 @LastFullBackup = BackupFile, @CurrentBackupPathFull = BackupPath + FROM @FileList + WHERE BackupFile LIKE N'%.bak' + AND + BackupFile LIKE N'%' + @Database + N'%' + AND + (@StopAt IS NULL OR REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) <= @StopAt) + ORDER BY BackupFile DESC; + + /* To get all backups that belong to the same set we can do two things: + 1. RESTORE HEADERONLY of ALL backup files in the folder and look for BackupSetGUID. + Backups that belong to the same split will have the same BackupSetGUID. + 2. Olla Hallengren's solution appends file index at the end of the name: + SQLSERVER1_TEST_DB_FULL_20180703_213211_1.bak + SQLSERVER1_TEST_DB_FULL_20180703_213211_2.bak + SQLSERVER1_TEST_DB_FULL_20180703_213211_N.bak + We can and find all related files with the same timestamp but different index. + This option is simpler and requires less changes to this procedure */ + + IF @LastFullBackup IS NULL + BEGIN + RAISERROR('No backups for "%s" found in "%s"', 16, 1, @Database, @BackupPathFull) WITH NOWAIT; + RETURN; + END; + + SELECT BackupPath, BackupFile INTO #SplitFullBackups + FROM @FileList + WHERE LEFT( BackupFile, LEN( BackupFile ) - PATINDEX( '%[_]%', REVERSE( BackupFile ) ) ) = LEFT( @LastFullBackup, LEN( @LastFullBackup ) - PATINDEX( '%[_]%', REVERSE( @LastFullBackup ) ) ) + AND PATINDEX( '%[_]%', REVERSE( @LastFullBackup ) ) <= 7 -- there is a 1 or 2 digit index at the end of the string which indicates split backups. Ola only supports up to 64 file split. + ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) DESC; + + -- File list can be obtained by running RESTORE FILELISTONLY of any file from the given BackupSet therefore we do not have to cater for split backups when building @FileListParamSQL + + SET @FileListParamSQL = + N'INSERT INTO #FileListParameters WITH (TABLOCK) + (LogicalName, PhysicalName, Type, FileGroupName, Size, MaxSize, FileID, CreateLSN, DropLSN + ,UniqueID, ReadOnlyLSN, ReadWriteLSN, BackupSizeInBytes, SourceBlockSize, FileGroupID, LogGroupGUID + ,DifferentialBaseLSN, DifferentialBaseGUID, IsReadOnly, IsPresent, TDEThumbprint'; + + IF @MajorVersion >= 13 + BEGIN + SET @FileListParamSQL += N', SnapshotUrl'; + END; + + SET @FileListParamSQL += N')' + NCHAR(13) + NCHAR(10); + SET @FileListParamSQL += N'EXEC (''RESTORE FILELISTONLY FROM DISK=''''{Path}'''''')'; + + SET @sql = REPLACE(@FileListParamSQL, N'{Path}', @CurrentBackupPathFull + @LastFullBackup); + + IF @Debug = 1 + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for INSERT to #FileListParameters: @BackupPathFull + @LastFullBackup'; + PRINT @sql; + END; + + EXEC (@sql); + IF @Debug = 1 + BEGIN + SELECT '#FileListParameters' AS table_name, * FROM #FileListParameters; + SELECT '@FileList' AS table_name, BackupPath, BackupFile FROM @FileList WHERE BackupFile IS NOT NULL; + END + + --get the backup completed data so we can apply tlogs from that point forwards + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathFull + @LastFullBackup); + + IF @Debug = 1 + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for get backup completed data: @BackupPathFull, @LastFullBackup'; + PRINT @sql; + END; + EXECUTE (@sql); + IF @Debug = 1 + BEGIN + SELECT '#Headers' AS table_name, @LastFullBackup AS FullBackupFile, * FROM #Headers + END; + + --Ensure we are looking at the expected backup, but only if we expect to restore a FULL backups + IF NOT EXISTS (SELECT * FROM #Headers h WHERE h.DatabaseName = @Database) + BEGIN + RAISERROR('Backupfile "%s" does not match @Database parameter "%s"', 16, 1, @LastFullBackup, @Database) WITH NOWAIT; + RETURN; + END; + + IF NOT @BufferCount IS NULL BEGIN - SELECT * - FROM @FileList; - END; - - + SET @BackupParameters += N', BufferCount=' + cast(@BufferCount as NVARCHAR(10)) + END -SET @FileListParamSQL = - N'INSERT INTO #FileListParameters WITH (TABLOCK) - (LogicalName, PhysicalName, Type, FileGroupName, Size, MaxSize, FileID, CreateLSN, DropLSN - ,UniqueID, ReadOnlyLSN, ReadWriteLSN, BackupSizeInBytes, SourceBlockSize, FileGroupID, LogGroupGUID - ,DifferentialBaseLSN, DifferentialBaseGUID, IsReadOnly, IsPresent, TDEThumbprint'; + IF NOT @MaxTransferSize IS NULL + BEGIN + SET @BackupParameters += N', MaxTransferSize=' + cast(@MaxTransferSize as NVARCHAR(7)) + END -IF @MajorVersion >= 13 + IF NOT @BlockSize IS NULL BEGIN - SET @FileListParamSQL += N', SnapshotUrl'; - END; + SET @BackupParameters += N', BlockSize=' + cast(@BlockSize as NVARCHAR(5)) + END -SET @FileListParamSQL += N')' + NCHAR(13) + NCHAR(10); -SET @FileListParamSQL += N'EXEC (''RESTORE FILELISTONLY FROM DISK=''''{Path}'''''')'; + IF @MoveFiles = 1 + BEGIN + IF @Execute = 'Y' RAISERROR('@MoveFiles = 1, adjusting paths', 0, 1) WITH NOWAIT; -SET @sql = REPLACE(@FileListParamSQL, N'{Path}', @BackupPathFull + @LastFullBackup); - - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for INSERT to #FileListParameters: @BackupPathFull + @LastFullBackup'; - PRINT @sql; - END; - -EXEC (@sql); + WITH Files + AS ( + SELECT + CASE + WHEN Type = 'D' THEN @MoveDataDrive + WHEN Type = 'L' THEN @MoveLogDrive + WHEN Type = 'S' THEN @MoveFilestreamDrive + WHEN Type = 'F' THEN @MoveFullTextCatalogDrive + END + COALESCE(@FileNamePrefix, '') + CASE + WHEN @Database = @RestoreDatabaseName THEN REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)) + ELSE REPLACE(REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)), @Database, SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) -2)) + END AS TargetPhysicalName, + PhysicalName, + LogicalName + FROM #FileListParameters) + SELECT @MoveOption = @MoveOption + N', MOVE ''' + Files.LogicalName + N''' TO ''' + Files.TargetPhysicalName + '''' + FROM Files + WHERE Files.TargetPhysicalName <> Files.PhysicalName; + + IF @Debug = 1 PRINT @MoveOption + END; + + /*Process @ExistingDBAction flag */ + IF @ExistingDBAction BETWEEN 1 AND 4 + BEGIN + IF @RestoreDatabaseID IS NOT NULL + BEGIN + IF @ExistingDBAction = 1 + BEGIN + RAISERROR('Setting single user', 0, 1) WITH NOWAIT; + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + ' SET SINGLE_USER WITH ROLLBACK IMMEDIATE; ' + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for SINGLE_USER'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') != 'RESTORING' + BEGIN + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'ALTER DATABASE SINGLE_USER', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE IF @Debug = 1 + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') IS NULL PRINT 'Unable to retrieve STATUS from "' + @UnquotedRestoreDatabaseName + '" database. Skipping setting database to SINGLE_USER'; + ELSE IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') = 'RESTORING' PRINT @UnquotedRestoreDatabaseName + ' database STATUS is RESTORING. Skiping setting database to SINGLE_USER'; + END + END + END + IF @ExistingDBAction IN (2, 3) + BEGIN + RAISERROR('Killing connections', 0, 1) WITH NOWAIT; + SET @sql = N'/* Kill connections */' + NCHAR(13); + SELECT + @sql = @sql + N'KILL ' + CAST(spid as nvarchar(5)) + N';' + NCHAR(13) + FROM + --database_ID was only added to sys.dm_exec_sessions in SQL Server 2012 but we need to support older + sys.sysprocesses + WHERE + dbid = @RestoreDatabaseID; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Kill connections'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'KILL CONNECTIONS', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + IF @ExistingDBAction = 3 + BEGIN + RAISERROR('Dropping database', 0, 1) WITH NOWAIT; + + SET @sql = N'DROP DATABASE ' + @RestoreDatabaseName + NCHAR(13); + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + IF @ExistingDBAction = 4 + BEGIN + RAISERROR ('Offlining database', 0, 1) WITH NOWAIT; - IF @Debug = 1 + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + SPACE( 1 ) + 'SET OFFLINE WITH ROLLBACK IMMEDIATE'; + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Offline database'; + PRINT @sql; + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') != 'RESTORING' + BEGIN + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'OFFLINE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE IF @Debug = 1 + BEGIN + IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') IS NULL PRINT 'Unable to retrieve STATUS from "' + @UnquotedRestoreDatabaseName + '" database. Skipping setting database OFFLINE'; + ELSE IF DATABASEPROPERTYEX(@UnquotedRestoreDatabaseName,'STATUS') = 'RESTORING' PRINT @UnquotedRestoreDatabaseName + ' database STATUS is RESTORING. Skiping setting database OFFLINE'; + END + END + END; + END + ELSE + RAISERROR('@ExistingDBAction > 0, but no existing @RestoreDatabaseName', 0, 1) WITH NOWAIT; + END + ELSE + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@ExistingDBAction %u so do nothing', 0, 1, @ExistingDBAction) WITH NOWAIT; + + IF @ContinueLogs = 0 + BEGIN + IF @Execute = 'Y' RAISERROR('@ContinueLogs set to 0', 0, 1) WITH NOWAIT; + + /* now take split backups into account */ + IF (SELECT COUNT(*) FROM #SplitFullBackups) > 0 + BEGIN + IF @Debug = 1 RAISERROR('Split backups found', 0, 1) WITH NOWAIT; + + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitFullBackups + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '') + N' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + ELSE BEGIN - SELECT '#FileListParameters' AS table_name, * FROM #FileListParameters + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathFull + @LastFullBackup + N''' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); END - - -SET @HeadersSQL = -N'INSERT INTO #Headers WITH (TABLOCK) - (BackupName, BackupDescription, BackupType, ExpirationDate, Compressed, Position, DeviceType, UserName, ServerName - ,DatabaseName, DatabaseVersion, DatabaseCreationDate, BackupSize, FirstLSN, LastLSN, CheckpointLSN, DatabaseBackupLSN - ,BackupStartDate, BackupFinishDate, SortOrder, CodePage, UnicodeLocaleId, UnicodeComparisonStyle, CompatibilityLevel - ,SoftwareVendorId, SoftwareVersionMajor, SoftwareVersionMinor, SoftwareVersionBuild, MachineName, Flags, BindingID - ,RecoveryForkID, Collation, FamilyGUID, HasBulkLoggedData, IsSnapshot, IsReadOnly, IsSingleUser, HasBackupChecksums - ,IsDamaged, BeginsLogChain, HasIncompleteMetaData, IsForceOffline, IsCopyOnly, FirstRecoveryForkID, ForkPointLSN - ,RecoveryModel, DifferentialBaseLSN, DifferentialBaseGUID, BackupTypeDescription, BackupSetGUID, CompressedBackupSize'; - -IF @MajorVersion >= 11 - SET @HeadersSQL += NCHAR(13) + NCHAR(10) + N', Containment'; - - -IF @MajorVersion >= 13 OR (@MajorVersion = 12 AND @BuildVersion >= 2342) - SET @HeadersSQL += N', KeyAlgorithm, EncryptorThumbprint, EncryptorType'; - -SET @HeadersSQL += N')' + NCHAR(13) + NCHAR(10); -SET @HeadersSQL += N'EXEC (''RESTORE HEADERONLY FROM DISK=''''{Path}'''''')'; - - -IF @MoveFiles = 1 - BEGIN - - RAISERROR('@MoveFiles = 1, adjusting paths', 0, 1) WITH NOWAIT; - - WITH Files - AS ( - SELECT N', MOVE ''' + LogicalName + N''' TO ''' + - CASE - WHEN Type = 'D' THEN @MoveDataDrive - WHEN Type = 'L' THEN @MoveLogDrive - END + CASE WHEN @Database = @RestoreDatabaseName THEN REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)) + '''' - ELSE REPLACE(REVERSE(LEFT(REVERSE(PhysicalName), CHARINDEX('\', REVERSE(PhysicalName), 1) -1)), @Database, SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) -2)) + '''' - END AS logicalcmds - FROM #FileListParameters) - - SELECT @MoveOption = @MoveOption + Files.logicalcmds - FROM Files; - - IF @Debug = 1 PRINT @MoveOption - - END; - - -IF @ContinueLogs = 0 - BEGIN - - RAISERROR('@ContinueLogs set to 0', 0, 1) WITH NOWAIT; - - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathFull + @LastFullBackup + N''' WITH NORECOVERY, REPLACE' + @MoveOption + NCHAR(13); - - IF @Debug = 1 + IF (@StandbyMode = 1) + BEGIN + IF (@StandbyUndoPath IS NULL) + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. The database will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END + ELSE IF (SELECT COUNT(*) FROM #SplitFullBackups) > 0 + BEGIN + SET @sql = @sql + ', STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + END + ELSE + BEGIN + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathFull + @LastFullBackup + N''' WITH REPLACE' + @BackupParameters + @MoveOption + N' , STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + END + END; + IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathFull, @LastFullBackup, @MoveOption'; PRINT @sql; END; - - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; - - --get the backup completed data so we can apply tlogs from that point forwards - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathFull + @LastFullBackup); - + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + + -- We already loaded #Headers above + + --setting the @BackupDateTime to a numeric string so that it can be used in comparisons + SET @BackupDateTime = REPLACE( RIGHT( REPLACE( @LastFullBackup, RIGHT( @LastFullBackup, PATINDEX( '%_[0-9][0-9]%', REVERSE( @LastFullBackup ) ) ), '' ), 16 ), '_', '' ); + + SELECT @FullLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) FROM #Headers WHERE BackupType = 1; IF @Debug = 1 BEGIN - IF @sql IS NULL PRINT '@sql is NULL for get backup completed data: @BackupPathFull, @LastFullBackup'; - PRINT @sql; + IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastFullBackup'; + PRINT @BackupDateTime; END; - - EXECUTE (@sql); - - --setting the @BackupDateTime to a numeric string so that it can be used in comparisons - SET @BackupDateTime = REPLACE(LEFT(RIGHT(@LastFullBackup, 19),15), '_', ''); - - SELECT @FullLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) FROM #Headers WHERE BackupType = 1; - IF @Debug = 1 - BEGIN - IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastFullBackup'; - PRINT @BackupDateTime; - END; - END; - -ELSE - + ELSE BEGIN - + SELECT @DatabaseLastLSN = CAST(f.redo_start_lsn AS NUMERIC(25, 0)) FROM master.sys.databases d JOIN master.sys.master_files f ON d.database_id = f.database_id WHERE d.name = SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) - 2) AND f.file_id = 1; - + END; +END; --- Clear out table variables for differential -DELETE FROM @FileList; +IF @BackupPathFull IS NULL AND @ContinueLogs = 1 +BEGIN -END + SELECT @DatabaseLastLSN = CAST(f.redo_start_lsn AS NUMERIC(25, 0)) + FROM master.sys.databases d + JOIN master.sys.master_files f ON d.database_id = f.database_id + WHERE d.name = SUBSTRING(@RestoreDatabaseName, 2, LEN(@RestoreDatabaseName) - 2) AND f.file_id = 1; +END; IF @BackupPathDiff IS NOT NULL - -BEGIN - --- Get list of files -SET @cmd = N'DIR /b "'+ @BackupPathDiff + N'"'; - - IF @Debug = 1 - BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathDiff check'; - PRINT @cmd; - END; - - IF @Debug = 1 +BEGIN + DELETE FROM @FileList; + DELETE FROM @FileListSimple; + DELETE FROM @PathItem; + + DECLARE @CurrentBackupPathDiff NVARCHAR(512); + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathDiff, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathDiff IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, EndPosition + 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathDiff, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathDiff, EndPosition + 1 ), 0 ), LEN( @BackupPathDiff ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathDiff ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 BEGIN - SELECT * - FROM @FileList; - END; - - -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; -/*Sanity check folders*/ - - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 + SELECT TOP 1 @CurrentBackupPathDiff = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathDiff, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @CurrentBackupPathDiff, 1, 1; + INSERT @FileList (BackupPath,BackupFile) SELECT @CurrentBackupPathDiff, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; - + SET @cmd = N'DIR /b "' + @CurrentBackupPathDiff + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathDiff'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathDiff WHERE BackupPath IS NULL; END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 - + IF @Debug = 1 BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathDiff) WITH NOWAIT; - + SELECT BackupPath,BackupFile FROM @FileList WHERE BackupFile IS NOT NULL; END; + IF @SimpleFolderEnumeration = 0 + BEGIN + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Bad value for path %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Access is denied to %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(DIFF) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathDiff) WITH NOWAIT; + RETURN; + END; + END; + END + /*End folder sanity check*/ + -- Find latest diff backup + IF @FileExtensionDiff IS NULL + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('No @FileExtensionDiff given, assuming "bak".', 0, 1) WITH NOWAIT; + SET @FileExtensionDiff = 'bak'; + END - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 + SELECT TOP 1 @LastDiffBackup = BackupFile, @CurrentBackupPathDiff = BackupPath + FROM @FileList + WHERE BackupFile LIKE N'%.' + @FileExtensionDiff + AND + BackupFile LIKE N'%' + @Database + '%' + AND + (@StopAt IS NULL OR REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) <= @StopAt) + ORDER BY BackupFile DESC; + + -- Load FileList data into Temp Table sorted by DateTime Stamp desc + SELECT BackupPath, BackupFile INTO #SplitDiffBackups + FROM @FileList + WHERE LEFT( BackupFile, LEN( BackupFile ) - PATINDEX( '%[_]%', REVERSE( BackupFile ) ) ) = LEFT( @LastDiffBackup, LEN( @LastDiffBackup ) - PATINDEX( '%[_]%', REVERSE( @LastDiffBackup ) ) ) + AND PATINDEX( '%[_]%', REVERSE( @LastDiffBackup ) ) <= 7 -- there is a 1 or 2 digit index at the end of the string which indicates split backups. Olla only supports up to 64 file split. + ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) DESC; + + --No file = no backup to restore + SET @LastDiffBackupDateTime = REPLACE( RIGHT( REPLACE( @LastDiffBackup, RIGHT( @LastDiffBackup, PATINDEX( '%_[0-9][0-9]%', REVERSE( @LastDiffBackup ) ) ), '' ), 16 ), '_', '' ); + + IF @RestoreDiff = 1 AND @BackupDateTime < @LastDiffBackupDateTime + BEGIN + IF (SELECT COUNT(*) FROM #SplitDiffBackups) > 0 BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END - -/*End folder sanity check*/ - - --- Find latest diff backup -SELECT @LastDiffBackup = MAX(BackupFile) -FROM @FileList -WHERE BackupFile LIKE N'%.bak' - AND - BackupFile LIKE N'%' + @Database + '%' - AND - (@StopAt IS NULL OR REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt); - - --set the @BackupDateTime so that it can be used for comparisons - SET @BackupDateTime = REPLACE(@BackupDateTime, '_', ''); - SET @LastDiffBackupDateTime = REPLACE(LEFT(RIGHT(@LastDiffBackup, 19),15), '_', ''); - + IF @Debug = 1 RAISERROR ('Split backups found', 0, 1) WITH NOWAIT; + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitDiffBackups + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '' ) + N' WITH NORECOVERY, REPLACE' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + ELSE + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathDiff + @LastDiffBackup + N''' WITH NORECOVERY' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); -IF @RestoreDiff = 1 AND @BackupDateTime < @LastDiffBackupDateTime - BEGIN - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH NORECOVERY' + NCHAR(13); - - IF @Debug = 1 + IF (@StandbyMode = 1) + BEGIN + IF (@StandbyUndoPath IS NULL) + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. The database will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END + ELSE IF (SELECT COUNT(*) FROM #SplitDiffBackups) > 0 + SET @sql = @sql + ', STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13) + NCHAR(10); + ELSE + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + @BackupParameters + @MoveOption + NCHAR(13) + NCHAR(10); + END; + IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @BackupPathDiff, @LastDiffBackup'; PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; - - --get the backup completed data so we can apply tlogs from that point forwards - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathDiff + @LastDiffBackup); - + END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + + --get the backup completed data so we can apply tlogs from that point forwards + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathDiff + @LastDiffBackup); + IF @Debug = 1 BEGIN - IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @BackupPathDiff, @LastDiffBackup'; + IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @CurrentBackupPathDiff, @LastDiffBackup'; PRINT @sql; - END; - - EXECUTE (@sql); - - IF @Debug = 1 - BEGIN - SELECT '#Headers' AS table_name, * FROM #Headers AS h - END - - --set the @BackupDateTime to the date time on the most recent differential - SET @BackupDateTime = @LastDiffBackupDateTime; - - SELECT @DiffLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) - FROM #Headers - WHERE BackupType = 5; - END; - --- Clear out table variables for translogs -DELETE FROM @FileList; - - END - - IF @BackupPathLog IS NOT NULL - -BEGIN - -SET @cmd = N'DIR /b "' + @BackupPathLog + N'"'; + END; + EXECUTE (@sql); IF @Debug = 1 BEGIN - IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathLog check'; - PRINT @cmd; - END; + SELECT '#Headers' AS table_name, @LastDiffBackup AS DiffbackupFile, * FROM #Headers AS h WHERE h.BackupType = 5; + END + --set the @BackupDateTime to the date time on the most recent differential + SET @BackupDateTime = ISNULL( @LastDiffBackupDateTime, @BackupDateTime ); IF @Debug = 1 BEGIN - SELECT * - FROM @FileList; + IF @BackupDateTime IS NULL PRINT '@BackupDateTime is NULL for REPLACE: @LastDiffBackupDateTime'; + PRINT @BackupDateTime; END; + SELECT @DiffLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) + FROM #Headers + WHERE BackupType = 5; + END; + IF @DiffLastLSN IS NULL + BEGIN + SET @DiffLastLSN=@FullLastLSN + END +END -INSERT INTO @FileList (BackupFile) -EXEC master.sys.xp_cmdshell @cmd; - -/*Sanity check folders*/ - - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'The system cannot find the path specified.' - OR fl.BackupFile = 'File Not Found' - ) = 1 +IF @BackupPathLog IS NOT NULL +BEGIN + DELETE FROM @FileList; + DELETE FROM @FileListSimple; + DELETE FROM @PathItem; + + DECLARE @CurrentBackupPathLog NVARCHAR(512); + -- Split CSV string logic has taken from Ola Hallengren's :) + WITH BackupPaths ( + StartPosition, EndPosition, PathItem + ) + AS ( + SELECT 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathLog, 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) - 1 ) AS PathItem + WHERE @BackupPathLog IS NOT NULL + UNION ALL + SELECT CAST( EndPosition AS INT ) + 1 AS StartPosition, + ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, EndPosition + 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) AS EndPosition, + SUBSTRING( @BackupPathLog, EndPosition + 1, ISNULL( NULLIF( CHARINDEX( ',', @BackupPathLog, EndPosition + 1 ), 0 ), LEN( @BackupPathLog ) + 1 ) - EndPosition - 1 ) AS PathItem + FROM BackupPaths + WHERE EndPosition < LEN( @BackupPathLog ) + 1 + ) + INSERT INTO @PathItem + SELECT CASE RIGHT( PathItem, 1 ) WHEN '\' THEN PathItem ELSE PathItem + '\' END FROM BackupPaths; + + WHILE 1 = 1 + BEGIN + SELECT TOP 1 @CurrentBackupPathLog = PathItem FROM @PathItem + WHERE PathItem > COALESCE( @CurrentBackupPathLog, '' ) ORDER BY PathItem; + IF @@rowcount = 0 BREAK; + + IF @SimpleFolderEnumeration = 1 + BEGIN -- Get list of files + INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @BackupPathLog, 1, 1; + INSERT @FileList (BackupPath, BackupFile) SELECT @CurrentBackupPathLog, BackupFile FROM @FileListSimple; + DELETE FROM @FileListSimple; + END + ELSE BEGIN - - RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; - + SET @cmd = N'DIR /b "' + @CurrentBackupPathLog + N'"'; + IF @Debug = 1 + BEGIN + IF @cmd IS NULL PRINT '@cmd is NULL for @CurrentBackupPathLog'; + PRINT @cmd; + END; + INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; + UPDATE @FileList SET BackupPath = @CurrentBackupPathLog + WHERE BackupPath IS NULL; END; - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile = 'Access is denied.' - ) = 1 - + IF @SimpleFolderEnumeration = 1 BEGIN - - RAISERROR('Access is denied to %s', 16, 1, @BackupPathLog) WITH NOWAIT; + /*Check what we can*/ + IF NOT EXISTS (SELECT * FROM @FileList) + BEGIN + RAISERROR('(LOG) No rows were returned for that database %s in path %s', 16, 1, @Database, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; + END + ELSE + BEGIN + /*Full Sanity check folders*/ + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The system cannot find the path specified.' + OR fl.BackupFile = 'File Not Found' + ) = 1 + BEGIN + RAISERROR('(LOG) No rows or bad value for path %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'Access is denied.' + ) = 1 + BEGIN + RAISERROR('(LOG) Access is denied to %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; + + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + ) = 1 + AND + ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile IS NULL + ) = 1 + BEGIN + RAISERROR('(LOG) Empty directory %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END + + IF ( + SELECT COUNT(*) + FROM @FileList AS fl + WHERE fl.BackupFile = 'The user name or password is incorrect.' + ) = 1 + BEGIN + RAISERROR('(LOG) Incorrect user name or password for %s', 16, 1, @CurrentBackupPathLog) WITH NOWAIT; + RETURN; + END; END; + END + /*End folder sanity check*/ - IF ( - SELECT COUNT(*) - FROM @FileList AS fl - ) = 1 - AND ( - SELECT COUNT(*) - FROM @FileList AS fl - WHERE fl.BackupFile IS NULL - ) = 1 +IF @Debug = 1 +BEGIN + SELECT * FROM @FileList WHERE BackupFile IS NOT NULL; +END - BEGIN - - RAISERROR('That directory appears to be empty', 0, 1) WITH NOWAIT; - - RETURN; - - END +IF @SkipBackupsAlreadyInMsdb = 1 +BEGIN -/*End folder sanity check*/ + SELECT TOP 1 @LogLastNameInMsdbAS = bf.physical_device_name + FROM msdb.dbo.backupmediafamily bf + INNER JOIN msdb.dbo.backupset bs ON bs.media_set_id = bf.media_set_id + INNER JOIN msdb.dbo.restorehistory rh ON rh.backup_set_id = bs.backup_set_id + WHERE physical_device_name like @BackupPathLog + '%' + AND rh.destination_database_name = @UnquotedRestoreDatabaseName + ORDER BY physical_device_name DESC -IF (@OnlyLogsAfter IS NOT NULL) + IF @Debug = 1 BEGIN - - RAISERROR('@OnlyLogsAfter is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; - - DELETE fl - FROM @FileList AS fl - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND REPLACE(LEFT(RIGHT(fl.BackupFile, 19), 15),'_','') < @OnlyLogsAfter - + SELECT 'Keeping LOG backups with name > : ' + @LogLastNameInMsdbAS + END + + DELETE fl + FROM @FileList AS fl + WHERE fl.BackupPath + fl.BackupFile <= @LogLastNameInMsdbAS END --- Check for log backups -IF(@StopAt IS NULL AND @OnlyLogsAfter IS NULL) - BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @BackupDateTime)) - ORDER BY BackupFile; - - OPEN BackupFiles; - END; +IF (@OnlyLogsAfter IS NOT NULL) +BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@OnlyLogsAfter is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; -IF (@StopAt IS NULL AND @OnlyLogsAfter IS NOT NULL) - BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @OnlyLogsAfter)) - ORDER BY BackupFile; - - OPEN BackupFiles; - END + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) < @OnlyLogsAfter; +END -IF (@StopAt IS NOT NULL AND @OnlyLogsAfter IS NULL) +-- Check for log backups +IF(@BackupDateTime IS NOT NULL AND @BackupDateTime <> '') BEGIN - DECLARE BackupFiles CURSOR FOR - SELECT BackupFile - FROM @FileList - WHERE BackupFile LIKE N'%.trn' - AND BackupFile LIKE N'%' + @Database + N'%' - AND (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') >= @BackupDateTime) AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt) - ORDER BY BackupFile; - - OPEN BackupFiles; + DELETE FROM @FileList + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND NOT (@ContinueLogs = 1 OR (@ContinueLogs = 0 AND REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' ) >= @BackupDateTime)); END; + IF (@StandbyMode = 1) BEGIN IF (@StandbyUndoPath IS NULL) - RAISERROR('The file path of the undo file for standby mode was not specified. Logs will not be restored in standby mode.', 0, 1) WITH NOWAIT; + BEGIN + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('The file path of the undo file for standby mode was not specified. Logs will not be restored in standby mode.', 0, 1) WITH NOWAIT; + END; ELSE SET @LogRecoveryOption = N'STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf'''; END; @@ -872,91 +1349,176 @@ IF (@LogRecoveryOption = N'') SET @LogRecoveryOption = N'NORECOVERY'; END; --- Loop through all the files for the database -FETCH NEXT FROM BackupFiles INTO @BackupFile; - WHILE @@FETCH_STATUS = 0 +IF (@StopAt IS NOT NULL) +BEGIN + + IF @Execute = 'Y' OR @Debug = 1 RAISERROR('@StopAt is NOT NULL, deleting from @FileList', 0, 1) WITH NOWAIT; + + IF LEN(@StopAt) <> 14 OR PATINDEX('%[^0-9]%', @StopAt) > 0 + BEGIN + RAISERROR('@StopAt parameter is incorrect. It should contain exactly 14 digits in the format yyyyMMddhhmmss.', 16, 1) WITH NOWAIT; + RETURN + END + + IF ISDATE(STUFF(STUFF(STUFF(@StopAt, 13, 0, ':'), 11, 0, ':'), 9, 0, ' ')) = 0 + BEGIN + RAISERROR('@StopAt is not a valid datetime.', 16, 1) WITH NOWAIT; + RETURN + END + + -- Add the STOPAT parameter to the log recovery options but change the value to a valid DATETIME, e.g. '20211118040230' -> '20211118 04:02:30' + SET @LogRecoveryOption += ', STOPAT = ''' + STUFF(STUFF(STUFF(@StopAt, 13, 0, ':'), 11, 0, ':'), 9, 0, ' ') + '''' + + IF @BackupDateTime = @StopAt + BEGIN + IF @Debug = 1 BEGIN + RAISERROR('@StopAt is the end time of a FULL backup, no log files will be restored.', 0, 1) WITH NOWAIT; + END + END + ELSE + BEGIN + DECLARE @ExtraLogFile NVARCHAR(255) + SELECT TOP 1 @ExtraLogFile = fl.BackupFile + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) > @StopAt + ORDER BY BackupFile; + END + + IF @ExtraLogFile IS NULL + BEGIN + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND REPLACE( RIGHT( REPLACE( fl.BackupFile, RIGHT( fl.BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( fl.BackupFile ) ) ), '' ), 16 ), '_', '' ) > @StopAt; + END + ELSE + BEGIN + -- If this is a split backup, @ExtraLogFile contains only the first split backup file, either _1.trn or _01.trn + -- Change @ExtraLogFile to the max split backup file, then delete all log files greater than this + SET @ExtraLogFile = REPLACE(REPLACE(@ExtraLogFile, '_1.trn', '_9.trn'), '_01.trn', '_64.trn') + + DELETE fl + FROM @FileList AS fl + WHERE BackupFile LIKE N'%.trn' + AND BackupFile LIKE N'%' + @Database + N'%' + AND fl.BackupFile > @ExtraLogFile + END +END + + + -- Group Ordering based on Backup File Name excluding Index {#} to construct coma separated string in "Restore Log" Command +SELECT BackupPath,BackupFile,DENSE_RANK() OVER (ORDER BY REPLACE( RIGHT( REPLACE( BackupFile, RIGHT( BackupFile, PATINDEX( '%_[0-9][0-9]%', REVERSE( BackupFile ) ) ), '' ), 16 ), '_', '' )) AS DenseRank INTO #SplitLogBackups +FROM @FileList +WHERE BackupFile IS NOT NULL; + +-- Loop through all the files for the database + WHILE 1 = 1 + BEGIN + + -- Get the TOP record to use in "Restore HeaderOnly/FileListOnly" statement + SELECT TOP 1 @CurrentBackupPathLog = BackupPath, @BackupFile = BackupFile FROM #SplitLogBackups WHERE DenseRank = @LogRestoreRanking; + IF @@rowcount = 0 BREAK; + IF @i = 1 - + BEGIN - SET @sql = REPLACE(@HeadersSQL, N'{Path}', @BackupPathLog + @BackupFile); - + SET @sql = REPLACE(@HeadersSQL, N'{Path}', @CurrentBackupPathLog + @BackupFile); + IF @Debug = 1 BEGIN - IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @HeadersSQL, @BackupPathLog, @BackupFile'; + IF @sql IS NULL PRINT '@sql is NULL for REPLACE: @HeadersSQL, @CurrentBackupPathLog, @BackupFile'; PRINT @sql; - END; - + END; + EXECUTE (@sql); - - SELECT @LogFirstLSN = CAST(FirstLSN AS NUMERIC(25, 0)), - @LogLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) - FROM #Headers - WHERE BackupType = 2; - - IF (@ContinueLogs = 0 AND @LogFirstLSN <= @FullLastLSN AND @FullLastLSN <= @LogLastLSN AND @RestoreDiff = 0) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 0) - SET @i = 2; - - IF (@ContinueLogs = 0 AND @LogFirstLSN <= @DiffLastLSN AND @DiffLastLSN <= @LogLastLSN AND @RestoreDiff = 1) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 1) - SET @i = 2; - - DELETE FROM #Headers WHERE BackupType = 2; + + SELECT TOP 1 @LogFirstLSN = CAST(FirstLSN AS NUMERIC(25, 0)), + @LogLastLSN = CAST(LastLSN AS NUMERIC(25, 0)) + FROM #Headers + WHERE BackupType = 2; + + IF (@ContinueLogs = 0 AND @LogFirstLSN <= @FullLastLSN AND @FullLastLSN <= @LogLastLSN AND @RestoreDiff = 0) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 0) + SET @i = 2; + + IF (@ContinueLogs = 0 AND @LogFirstLSN <= @DiffLastLSN AND @DiffLastLSN <= @LogLastLSN AND @RestoreDiff = 1) OR (@ContinueLogs = 1 AND @LogFirstLSN <= @DatabaseLastLSN AND @DatabaseLastLSN < @LogLastLSN AND @RestoreDiff = 1) + SET @i = 2; + + DELETE FROM #Headers WHERE BackupType = 2; END; IF @i = 1 BEGIN - IF @Debug = 1 RAISERROR('No Log to Restore', 0, 1) WITH NOWAIT; - END IF @i = 2 BEGIN + IF @Execute = 'Y' RAISERROR('@i set to 2, restoring logs', 0, 1) WITH NOWAIT; - RAISERROR('@i set to 2, restoring logs', 0, 1) WITH NOWAIT; - - SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathLog + @BackupFile + N''' WITH ' + @LogRecoveryOption + NCHAR(13); - - IF @Debug = 1 + IF (SELECT COUNT( * ) FROM #SplitLogBackups WHERE DenseRank = @LogRestoreRanking) > 1 + BEGIN + IF @Debug = 1 RAISERROR ('Split backups found', 0, 1) WITH NOWAIT; + SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM ' + + STUFF( + (SELECT CHAR( 10 ) + ',DISK=''' + BackupPath + BackupFile + '''' + FROM #SplitLogBackups + WHERE DenseRank = @LogRestoreRanking + ORDER BY BackupFile + FOR XML PATH ('')), + 1, + 2, + '' ) + N' WITH ' + @LogRecoveryOption + NCHAR(13) + NCHAR(10); + END; + ELSE + SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM DISK = ''' + @CurrentBackupPathLog + @BackupFile + N''' WITH ' + @LogRecoveryOption + NCHAR(13) + NCHAR(10); + + IF @Debug = 1 OR @Execute = 'N' BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE LOG: @RestoreDatabaseName, @BackupPathLog, @BackupFile'; + IF @sql IS NULL PRINT '@sql is NULL for RESTORE LOG: @RestoreDatabaseName, @CurrentBackupPathLog, @BackupFile'; PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE LOG', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; - END; - - FETCH NEXT FROM BackupFiles INTO @BackupFile; - END; - - CLOSE BackupFiles; + END; -DEALLOCATE BackupFiles; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RESTORE LOG', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; - IF @Debug = 1 - BEGIN - SELECT '#Headers' AS table_name, * FROM #Headers AS h - END + SET @LogRestoreRanking += 1; + END; + IF @Debug = 1 + BEGIN + SELECT '#SplitLogBackups' AS table_name, BackupPath, BackupFile FROM #SplitLogBackups; + END END --- Put database in a useable state +-- Put database in a useable state IF @RunRecovery = 1 BEGIN - SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' WITH RECOVERY' + NCHAR(13); + SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' WITH RECOVERY'; - IF @Debug = 1 - BEGIN - IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @RestoreDatabaseName'; - PRINT @sql; - END; + IF @KeepCdc = 1 + SET @sql = @sql + N', KEEP_CDC'; + + IF @EnableBroker = 1 + SET @sql = @sql + N', ENABLE_BROKER'; + + SET @sql = @sql + NCHAR(13); + + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @RestoreDatabaseName'; + PRINT @sql; + END; - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'RECOVER DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; END; -- Ensure simple recovery model @@ -964,46 +1526,166 @@ IF @ForceSimpleRecovery = 1 BEGIN SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + N' SET RECOVERY SIMPLE' + NCHAR(13); - IF @Debug = 1 + IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for SET RECOVERY SIMPLE: @RestoreDatabaseName'; PRINT @sql; - END; + END; - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; - END; + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'SIMPLE LOGGING', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; -- Run checkdb against this database IF @RunCheckDB = 1 BEGIN - SET @sql = N'EXECUTE [dbo].[DatabaseIntegrityCheck] @Databases = ' + @RestoreDatabaseName + N', @LogToTable = ''Y''' + NCHAR(13); - - IF @Debug = 1 + SET @sql = N'DBCC CHECKDB (' + @RestoreDatabaseName + N') WITH NO_INFOMSGS, ALL_ERRORMSGS, DATA_PURITY;'; + + IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for Run Integrity Check: @RestoreDatabaseName'; PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sys.sp_executesql @sql; + END; + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'DBCC CHECKDB', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END; + + +IF @DatabaseOwner IS NOT NULL + BEGIN + IF @RunRecovery = 1 + BEGIN + IF EXISTS (SELECT * FROM master.dbo.syslogins WHERE syslogins.loginname = @DatabaseOwner) + BEGIN + SET @sql = N'ALTER AUTHORIZATION ON DATABASE::' + @RestoreDatabaseName + ' TO [' + @DatabaseOwner + ']'; + + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Set Database Owner'; + PRINT @sql; + END; + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master',@Command = @sql, @CommandType = 'ALTER AUTHORIZATION', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE + BEGIN + PRINT @DatabaseOwner + ' is not a valid Login. Database Owner not set.'; + END + END + ELSE + BEGIN + PRINT @RestoreDatabaseName + ' is still in Recovery, so we are unable to change the database owner to [' + @DatabaseOwner + '].'; + END + END; + + IF @SetTrustworthyON = 1 + BEGIN + IF @RunRecovery = 1 + BEGIN + IF IS_SRVROLEMEMBER('sysadmin') = 1 + BEGIN + SET @sql = N'ALTER DATABASE ' + @RestoreDatabaseName + N' SET TRUSTWORTHY ON;'; + + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for SET TRUSTWORTHY ON'; + PRINT @sql; + END; + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master', @Command = @sql, @CommandType = 'ALTER DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; + END + ELSE + BEGIN + PRINT 'Current user''s login is NOT a member of the sysadmin role. Database TRUSTWORHY bit has not been enabled.'; + END + END + ELSE + BEGIN + PRINT @RestoreDatabaseName + ' is still in Recovery, so we are unable to enable the TRUSTWORHY bit.'; + END + END; + +-- Link a user entry in the sys.database_principals system catalog view in the restored database to a SQL Server login of the same name +IF @FixOrphanUsers = 1 + BEGIN + SET @sql = N' +-- Fixup Orphan Users by setting database user sid to match login sid +DECLARE @FixOrphansSql NVARCHAR(MAX); +DECLARE @OrphanUsers TABLE (SqlToExecute NVARCHAR(MAX)); +USE ' + @RestoreDatabaseName + '; + +INSERT @OrphanUsers +SELECT ''ALTER USER ['' + d.name + ''] WITH LOGIN = ['' + d.name + '']; '' + FROM sys.database_principals d + INNER JOIN master.sys.server_principals s ON d.name COLLATE DATABASE_DEFAULT = s.name COLLATE DATABASE_DEFAULT + WHERE d.type_desc = ''SQL_USER'' + AND d.name NOT IN (''guest'',''dbo'') + AND d.sid <> s.sid + ORDER BY d.name; + +SELECT @FixOrphansSql = (SELECT SqlToExecute AS [text()] FROM @OrphanUsers FOR XML PATH (''''), TYPE).value(''text()[1]'',''NVARCHAR(MAX)''); + +IF @FixOrphansSql IS NULL + PRINT ''No orphan users require a sid fixup.''; +ELSE +BEGIN + PRINT ''Fix Orphan Users: '' + @FixOrphansSql; + EXECUTE(@FixOrphansSql); +END;' + + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL for Fix Orphan Users'; + PRINT @sql; + END; + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE [dbo].[CommandExecute] @DatabaseContext = 'master', @Command = @sql, @CommandType = 'UPDATE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; END; - -- If test restore then blow the database away (be careful) +IF @RunStoredProcAfterRestore IS NOT NULL AND LEN(LTRIM(@RunStoredProcAfterRestore)) > 0 +BEGIN + PRINT 'Attempting to run ' + @RunStoredProcAfterRestore + SET @sql = N'EXEC ' + @RestoreDatabaseName + '.' + @RunStoredProcAfterRestore + + IF @Debug = 1 OR @Execute = 'N' + BEGIN + IF @sql IS NULL PRINT '@sql is NULL when building for @RunStoredProcAfterRestore' + PRINT @sql + END + + IF @RunRecovery = 0 + BEGIN + PRINT 'Unable to run Run Stored Procedure After Restore as database is not recovered. Run command again with @RunRecovery = 1' + END + ELSE + BEGIN + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXEC sp_executesql @sql + END +END + +-- If test restore then blow the database away (be careful) IF @TestRestore = 1 BEGIN SET @sql = N'DROP DATABASE ' + @RestoreDatabaseName + NCHAR(13); - - IF @Debug = 1 + + IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE: @RestoreDatabaseName'; PRINT @sql; - END; - - IF @Debug IN (0, 1) - EXECUTE sp_executesql @sql; + END; + + IF @Debug IN (0, 1) AND @Execute = 'Y' + EXECUTE @sql = [dbo].[CommandExecute] @DatabaseContext=N'master',@Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @UnquotedRestoreDatabaseName, @LogToTable = 'Y', @Execute = 'Y'; END; +-- Clean-Up Tempdb Objects +IF OBJECT_ID( 'tempdb..#SplitFullBackups' ) IS NOT NULL DROP TABLE #SplitFullBackups; +IF OBJECT_ID( 'tempdb..#SplitDiffBackups' ) IS NOT NULL DROP TABLE #SplitDiffBackups; +IF OBJECT_ID( 'tempdb..#SplitLogBackups' ) IS NOT NULL DROP TABLE #SplitLogBackups; GO - diff --git a/sp_foreachdb.sql b/sp_foreachdb.sql deleted file mode 100644 index 26bc8de5e..000000000 --- a/sp_foreachdb.sql +++ /dev/null @@ -1,223 +0,0 @@ -USE [master]; -GO - -IF OBJECT_ID('dbo.sp_foreachdb') IS NULL - EXEC ('CREATE PROCEDURE dbo.sp_foreachdb AS RETURN 0'); -GO - -ALTER PROCEDURE dbo.sp_foreachdb - -- Original fields from sp_MSforeachdb... - @command1 NVARCHAR(MAX) = NULL, - @replacechar NCHAR(1) = N'?' , - @command2 NVARCHAR(MAX) = NULL , - @command3 NVARCHAR(MAX) = NULL , - @precommand NVARCHAR(MAX) = NULL , - @postcommand NVARCHAR(MAX) = NULL , - -- Additional fields for our sp_foreachdb! - @command NVARCHAR(MAX) = NULL, --For backwards compatibility - @print_dbname BIT = 0 , - @print_command_only BIT = 0 , - @suppress_quotename BIT = 0 , - @system_only BIT = NULL , - @user_only BIT = NULL , - @name_pattern NVARCHAR(300) = N'%' , - @database_list NVARCHAR(MAX) = NULL , - @exclude_list NVARCHAR(MAX) = NULL , - @recovery_model_desc NVARCHAR(120) = NULL , - @compatibility_level TINYINT = NULL , - @state_desc NVARCHAR(120) = N'ONLINE' , - @is_read_only BIT = 0 , - @is_auto_close_on BIT = NULL , - @is_auto_shrink_on BIT = NULL , - @is_broker_enabled BIT = NULL , - @VersionDate DATETIME = NULL OUTPUT -AS - BEGIN - SET NOCOUNT ON; - DECLARE @Version VARCHAR(30); - SET @Version = '2.0'; - SET @VersionDate = '20171201'; - - IF ( (@command1 IS NOT NULL AND @command IS NOT NULL) - OR (@command1 IS NULL AND @command IS NULL) ) - BEGIN - RAISERROR('You must supply either @command1 or @command, but not both.',16,1); - RETURN -1; - END; - - SET @command1 = COALESCE(@command1,@command); - - DECLARE @sql NVARCHAR(MAX) , - @dblist NVARCHAR(MAX) , - @exlist NVARCHAR(MAX) , - @db NVARCHAR(300) , - @i INT; - - IF @database_list > N'' - BEGIN - ; - WITH n ( n ) - AS ( SELECT ROW_NUMBER() OVER ( ORDER BY s1.name ) - - 1 - FROM sys.objects AS s1 - CROSS JOIN sys.objects AS s2 - ) - SELECT @dblist = REPLACE(REPLACE(REPLACE(x, '', - ','), '', ''), - '', '') - FROM ( SELECT DISTINCT - x = 'N''' - + LTRIM(RTRIM(SUBSTRING(@database_list, - n, - CHARINDEX(',', - @database_list - + ',', n) - n))) - + '''' - FROM n - WHERE n <= LEN(@database_list) - AND SUBSTRING(',' + @database_list, n, - 1) = ',' - FOR - XML PATH('') - ) AS y ( x ); - END --- Added for @exclude_list - IF @exclude_list > N'' - BEGIN - ; - WITH n ( n ) - AS ( SELECT ROW_NUMBER() OVER ( ORDER BY s1.name ) - - 1 - FROM sys.objects AS s1 - CROSS JOIN sys.objects AS s2 - ) - SELECT @exlist = REPLACE(REPLACE(REPLACE(x, '', - ','), '', ''), - '', '') - FROM ( SELECT DISTINCT - x = 'N''' - + LTRIM(RTRIM(SUBSTRING(@exclude_list, - n, - CHARINDEX(',', - @exclude_list - + ',', n) - n))) - + '''' - FROM n - WHERE n <= LEN(@exclude_list) - AND SUBSTRING(',' + @exclude_list, n, - 1) = ',' - FOR - XML PATH('') - ) AS y ( x ); - END - - CREATE TABLE #x ( db NVARCHAR(300) ); - - SET @sql = N'SELECT name FROM sys.databases d WHERE 1=1' - + CASE WHEN @system_only = 1 THEN ' AND d.database_id IN (1,2,3,4)' - ELSE '' - END - + CASE WHEN @user_only = 1 - THEN ' AND d.database_id NOT IN (1,2,3,4)' - ELSE '' - END --- To exclude databases from changes - + CASE WHEN @exlist IS NOT NULL - THEN ' AND d.name NOT IN (' + @exlist + ')' - ELSE '' - END + CASE WHEN @name_pattern <> N'%' - THEN ' AND d.name LIKE N''' + REPLACE(@name_pattern, - '''', '''''') - + '''' - ELSE '' - END + CASE WHEN @dblist IS NOT NULL - THEN ' AND d.name IN (' + @dblist + ')' - ELSE '' - END - + CASE WHEN @recovery_model_desc IS NOT NULL - THEN ' AND d.recovery_model_desc = N''' - + @recovery_model_desc + '''' - ELSE '' - END - + CASE WHEN @compatibility_level IS NOT NULL - THEN ' AND d.compatibility_level = ' - + RTRIM(@compatibility_level) - ELSE '' - END - + CASE WHEN @state_desc IS NOT NULL - THEN ' AND d.state_desc = N''' + @state_desc + '''' - ELSE '' - END - + CASE WHEN @state_desc = 'ONLINE' AND SERVERPROPERTY('IsHadrEnabled') = 1 - THEN ' AND NOT EXISTS (SELECT 1 - FROM sys.dm_hadr_database_replica_states drs - JOIN sys.availability_replicas ar - ON ar.replica_id = drs.replica_id - JOIN sys.dm_hadr_availability_group_states ags - ON ags.group_id = ar.group_id - WHERE drs.database_id = d.database_id - AND ar.secondary_role_allow_connections = 0 - AND ags.primary_replica <> @@SERVERNAME)' - ELSE '' - END - + CASE WHEN @is_read_only IS NOT NULL - THEN ' AND d.is_read_only = ' + RTRIM(@is_read_only) - ELSE '' - END - + CASE WHEN @is_auto_close_on IS NOT NULL - THEN ' AND d.is_auto_close_on = ' + RTRIM(@is_auto_close_on) - ELSE '' - END - + CASE WHEN @is_auto_shrink_on IS NOT NULL - THEN ' AND d.is_auto_shrink_on = ' + RTRIM(@is_auto_shrink_on) - ELSE '' - END - + CASE WHEN @is_broker_enabled IS NOT NULL - THEN ' AND d.is_broker_enabled = ' + RTRIM(@is_broker_enabled) - ELSE '' - END; - - INSERT #x - EXEC sp_executesql @sql; - - DECLARE c CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY - FOR - SELECT CASE WHEN @suppress_quotename = 1 THEN db - ELSE QUOTENAME(db) - END - FROM #x - ORDER BY db; - - OPEN c; - - FETCH NEXT FROM c INTO @db; - - WHILE @@FETCH_STATUS = 0 - BEGIN - SET @sql = REPLACE(@command1, @replacechar, @db); - - IF @suppress_quotename = 0 SET @sql = REPLACE(REPLACE(@sql,'[[','['),']]',']'); - - IF @print_command_only = 1 - BEGIN - PRINT '/* For ' + @db + ': */' + CHAR(13) + CHAR(10) - + CHAR(13) + CHAR(10) + @sql + CHAR(13) + CHAR(10) - + CHAR(13) + CHAR(10); - END - ELSE - BEGIN - IF @print_dbname = 1 - BEGIN - PRINT '/* ' + @db + ' */'; - END - - EXEC sp_executesql @sql; - END - - FETCH NEXT FROM c INTO @db; - END - - CLOSE c; - DEALLOCATE c; - END -GO diff --git a/sp_ineachdb.sql b/sp_ineachdb.sql new file mode 100644 index 000000000..eca2b7a98 --- /dev/null +++ b/sp_ineachdb.sql @@ -0,0 +1,375 @@ +IF OBJECT_ID('dbo.sp_ineachdb') IS NULL + EXEC ('CREATE PROCEDURE dbo.sp_ineachdb AS RETURN 0') +GO + +ALTER PROCEDURE [dbo].[sp_ineachdb] + -- mssqltips.com/sqlservertip/5694/execute-a-command-in-the-context-of-each-database-in-sql-server--part-2/ + @command nvarchar(max) = NULL, + @replace_character nchar(1) = N'?', + @print_dbname bit = 0, + @select_dbname bit = 0, + @print_command bit = 0, + @print_command_only bit = 0, + @suppress_quotename bit = 0, -- use with caution + @system_only bit = 0, + @user_only bit = 0, + @name_pattern nvarchar(300) = N'%', + @database_list nvarchar(max) = NULL, + @exclude_pattern nvarchar(300) = NULL, + @exclude_list nvarchar(max) = NULL, + @recovery_model_desc nvarchar(120) = NULL, + @compatibility_level tinyint = NULL, + @state_desc nvarchar(120) = N'ONLINE', + @is_read_only bit = 0, + @is_auto_close_on bit = NULL, + @is_auto_shrink_on bit = NULL, + @is_broker_enabled bit = NULL, + @user_access nvarchar(128) = NULL, + @Help bit = 0, + @Version varchar(30) = NULL OUTPUT, + @VersionDate datetime = NULL OUTPUT, + @VersionCheckMode bit = 0, + @is_ag_writeable_copy 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.26', @VersionDate = '20251002'; + + IF(@VersionCheckMode = 1) + BEGIN + RETURN; + END; + + IF @Help = 1 + + BEGIN + + PRINT ' + /* + sp_ineachdb from http://FirstResponderKit.org + + This script will execute a command against multiple databases. + + 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. + + Known limitations of this version: + - Only Microsoft-supported versions of SQL Server. Sorry, 2005 and 2000. + - Tastes awful with marmite. + + Unknown limitations of this version: + - None. (If we knew them, they would be known. Duh.) + + Changes - for the full list of improvements and fixes in this version, see: + https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/ + + MIT License + + Copyright (c) Brent Ozar Unlimited + + 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. + + */ + '; + + RETURN -1; + END + + DECLARE @exec nvarchar(150), + @sx nvarchar(18) = N'.sys.sp_executesql', + @db sysname, + @dbq sysname, + @cmd nvarchar(max), + @thisdb sysname, + @cr char(2) = CHAR(13) + CHAR(10), + @SQLVersion AS tinyint = (@@microsoftversion / 0x1000000) & 0xff, -- Stores the SQL Server Version Number(8(2000),9(2005),10(2008 & 2008R2),11(2012),12(2014),13(2016),14(2017),15(2019) + @ServerName AS sysname = CONVERT(sysname, SERVERPROPERTY('ServerName')), -- Stores the SQL Server Instance name. + @NoSpaces nvarchar(20) = N'%[^' + CHAR(9) + CHAR(32) + CHAR(10) + CHAR(13) + N']%'; --Pattern for PATINDEX + + + CREATE TABLE #ineachdb(id int, name nvarchar(512), is_distributor bit); + + +/* + -- first, let's limit to only DBs the caller is interested in + IF @database_list > N'' + -- comma-separated list of potentially valid/invalid/quoted/unquoted names + BEGIN + ;WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n <= LEN(@database_list)), + names AS + ( + SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@database_list, n, + CHARINDEX(N',', @database_list + N',', n) - n), 1))) + FROM n + WHERE SUBSTRING(N',' + @database_list, n, 1) = N',' + ) + INSERT #ineachdb(id,name,is_distributor) + SELECT d.database_id, d.name, d.is_distributor + FROM sys.databases AS d + WHERE EXISTS (SELECT 1 FROM names WHERE name = d.name) + OPTION (MAXRECURSION 0); + END + ELSE + BEGIN + INSERT #ineachdb(id,name,is_distributor) SELECT database_id, name, is_distributor FROM sys.databases; + END + + -- now delete any that have been explicitly excluded - exclude trumps include + IF @exclude_list > N'' + -- comma-separated list of potentially valid/invalid/quoted/unquoted names + BEGIN + ;WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n <= LEN(@exclude_list)), + names AS + ( + SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@exclude_list, n, + CHARINDEX(N',', @exclude_list + N',', n) - n), 1))) + FROM n + WHERE SUBSTRING(N',' + @exclude_list, n, 1) = N',' + ) + DELETE d + FROM #ineachdb AS d + INNER JOIN names + ON names.name = d.name + OPTION (MAXRECURSION 0); + END +*/ + +/* +@database_list and @exclude_list are are processed at the same time +1)Read the list searching for a comma or [ +2)If we find a comma, save the name +3)If we find a [, we begin to accumulate the result until we reach closing ], (jumping over escaped ]]). +4)Finally, tabs, line breaks and spaces are removed from unquoted names +*/ +WITH C +AS (SELECT V.SrcList + , CAST('' AS nvarchar(MAX)) AS Name + , V.DBList + , 0 AS InBracket + , 0 AS Quoted + FROM (VALUES ('In', @database_list + ','), ('Out', @exclude_list + ',')) AS V (SrcList, DBList) + UNION ALL + SELECT C.SrcList +-- , IIF(V.Found = '[', '', SUBSTRING(C.DBList, 1, V.Place - 1))/*remove initial [*/ + , CASE WHEN V.Found = '[' THEN '' ELSE SUBSTRING(C.DBList, 1, V.Place - 1) END /*remove initial [*/ + , STUFF(C.DBList, 1, V.Place, '') +-- , IIF(V.Found = '[', 1, 0) + ,Case WHEN V.Found = '[' THEN 1 ELSE 0 END + , 0 + FROM C + CROSS APPLY + ( VALUES (PATINDEX('%[,[]%', C.DBList), SUBSTRING(C.DBList, PATINDEX('%[,[]%', C.DBList), 1))) AS V (Place, Found) + WHERE C.DBList > '' + AND C.InBracket = 0 + UNION ALL + SELECT C.SrcList +-- , CONCAT(C.Name, SUBSTRING(C.DBList, 1, V.Place + W.DoubleBracket - 1)) /*Accumulates only one ] if escaped]] or none if end]*/ + , ISNULL(C.Name,'') + ISNULL(SUBSTRING(C.DBList, 1, V.Place + W.DoubleBracket - 1),'') /*Accumulates only one ] if escaped]] or none if end]*/ + , STUFF(C.DBList, 1, V.Place + W.DoubleBracket, '') + , W.DoubleBracket + , 1 + FROM C + CROSS APPLY (VALUES (CHARINDEX(']', C.DBList))) AS V (Place) + -- CROSS APPLY (VALUES (IIF(SUBSTRING(C.DBList, V.Place + 1, 1) = ']', 1, 0))) AS W (DoubleBracket) + CROSS APPLY (VALUES (CASE WHEN SUBSTRING(C.DBList, V.Place + 1, 1) = ']' THEN 1 ELSE 0 END)) AS W (DoubleBracket) + WHERE C.DBList > '' + AND C.InBracket = 1) + , F +AS (SELECT C.SrcList + , CASE WHEN C.Quoted = 0 THEN + SUBSTRING(C.Name, PATINDEX(@NoSpaces, Name), DATALENGTH (Name)/2 - PATINDEX(@NoSpaces, Name) - PATINDEX(@NoSpaces, REVERSE(Name))+2) + ELSE C.Name END + AS name + FROM C + WHERE C.InBracket = 0 + AND C.Name > '') +INSERT #ineachdb(id,name,is_distributor) +SELECT d.database_id + , d.name + , d.is_distributor +FROM sys.databases AS d +WHERE ( EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'In') + OR @database_list IS NULL) + AND NOT EXISTS (SELECT NULL FROM F WHERE F.name = d.name AND F.SrcList = 'Out') +OPTION (MAXRECURSION 0); +; + -- next, let's delete any that *don't* match various criteria passed in + DELETE dbs FROM #ineachdb AS dbs + WHERE (@system_only = 1 AND (id NOT IN (1,2,3,4) AND is_distributor <> 1)) + OR (@user_only = 1 AND (id IN (1,2,3,4) OR is_distributor = 1)) + OR name NOT LIKE @name_pattern + OR name LIKE @exclude_pattern + OR EXISTS + ( + SELECT 1 + FROM sys.databases AS d + WHERE d.database_id = dbs.id + AND NOT + ( + recovery_model_desc = COALESCE(@recovery_model_desc, recovery_model_desc) + AND compatibility_level = COALESCE(@compatibility_level, compatibility_level) + AND is_read_only = COALESCE(@is_read_only, is_read_only) + AND is_auto_close_on = COALESCE(@is_auto_close_on, is_auto_close_on) + AND is_auto_shrink_on = COALESCE(@is_auto_shrink_on, is_auto_shrink_on) + AND is_broker_enabled = COALESCE(@is_broker_enabled, is_broker_enabled) + ) + ); + + -- 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 + DELETE #ineachdb WHERE + CONVERT(nvarchar(128), DATABASEPROPERTYEX(name, 'UserAccess')) <> @user_access; + END + + -- finally, remove any that are not *fully* online or we can't access + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.databases + WHERE database_id = dbs.id + AND + ( + @state_desc = N'ONLINE' AND + ( + [state] & 992 <> 0 -- inaccessible + OR state_desc <> N'ONLINE' -- not online + OR HAS_DBACCESS(name) = 0 -- don't have access + OR DATABASEPROPERTYEX(name, 'Collation') IS NULL -- not fully online. See "status" here: + -- https://docs.microsoft.com/en-us/sql/t-sql/functions/databasepropertyex-transact-sql + ) + OR (@state_desc <> N'ONLINE' AND state_desc <> @state_desc) + ) + ); + + -- from Andy Mallon / First Responders Kit. Make sure that if we're an + -- AG secondary, we skip any database where allow connections is off + IF @SQLVersion >= 11 AND 3 = (SELECT COUNT(*) FROM sys.all_objects WHERE name IN('availability_replicas','dm_hadr_availability_group_states','dm_hadr_database_replica_states')) + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.dm_hadr_database_replica_states AS drs + INNER JOIN sys.availability_replicas AS ar + ON ar.replica_id = drs.replica_id + INNER JOIN sys.dm_hadr_availability_group_states ags + ON ags.group_id = ar.group_id + WHERE drs.database_id = dbs.id + AND ar.secondary_role_allow_connections = 0 + AND ags.primary_replica <> @ServerName + ); + /* Remove databases which are not the writeable copies in an AG. */ + IF @is_ag_writeable_copy = 1 + BEGIN + DELETE dbs FROM #ineachdb AS dbs + WHERE EXISTS + ( + SELECT 1 FROM sys.dm_hadr_database_replica_states AS drs + INNER JOIN sys.availability_replicas AS ar + ON ar.replica_id = drs.replica_id + INNER JOIN sys.dm_hadr_availability_group_states AS ags + ON ags.group_id = ar.group_id + WHERE drs.database_id = dbs.id + AND drs.is_primary_replica <> 1 + AND ags.primary_replica <> @ServerName + ); + END + END + + -- Well, if we deleted them all... + IF NOT EXISTS (SELECT 1 FROM #ineachdb) + BEGIN + RAISERROR(N'No databases to process.', 1, 0); + RETURN; + END + + -- ok, now, let's go through what we have left + DECLARE dbs CURSOR LOCAL FAST_FORWARD + FOR SELECT DB_NAME(id), QUOTENAME(DB_NAME(id)) + FROM #ineachdb; + + OPEN dbs; + + FETCH NEXT FROM dbs INTO @db, @dbq; + + DECLARE @msg1 nvarchar(512) = N'Could not run against %s : %s.', + @msg2 nvarchar(max); + + WHILE @@FETCH_STATUS <> -1 + BEGIN + SET @thisdb = CASE WHEN @suppress_quotename = 1 THEN @db ELSE @dbq END; + SET @cmd = REPLACE(@command, @replace_character, REPLACE(@thisdb,'''','''''')); + + BEGIN TRY + IF @print_dbname = 1 + BEGIN + PRINT N'/* ' + @thisdb + N' */'; + END + + IF @select_dbname = 1 + BEGIN + SELECT [ineachdb current database] = @thisdb; + END + + IF 1 IN (@print_command, @print_command_only) + BEGIN + PRINT N'/* For ' + @thisdb + ': */' + @cr + @cr + @cmd + @cr + @cr; + END + + IF COALESCE(@print_command_only,0) = 0 + BEGIN + SET @exec = @dbq + @sx; + EXEC @exec @cmd; + END + END TRY + + BEGIN CATCH + SET @msg2 = ERROR_MESSAGE(); + RAISERROR(@msg1, 1, 0, @db, @msg2); + END CATCH + + FETCH NEXT FROM dbs INTO @db, @dbq; + END + + CLOSE dbs; + DEALLOCATE dbs; +END +GO