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;') 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, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; BEGIN; SELECT @Version = '3.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; IF @Help = 1 BEGIN PRINT ' /* 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) 2019 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 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'' /* Make sure we're doing something */ 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 /* 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 IF (@PollDiskForNewDatabases = 1 OR @Restore = 1) AND OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL BEGIN IF @Debug = 1 RAISERROR('Checking restore path', 0, 1) WITH NOWAIT; SELECT @restore_path_base = CONVERT(NVARCHAR(512), configuration_setting) FROM msdb.dbo.restore_configuration c WHERE configuration_name = N'log restore path'; IF @restore_path_base IS NULL BEGIN RAISERROR('@restore_path cannot be NULL. Please check the msdb.dbo.restore_configuration table', 0, 1) WITH NOWAIT; RETURN; END; IF CHARINDEX('**', @restore_path_base) <> 0 BEGIN /* 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 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; END /* IF CHARINDEX('**', @restore_path_base) <> 0 */ END /* IF @PollDiskForNewDatabases = 1 OR @Restore = 1 */ /* 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 @PollForNewDatabases = 1 GOTO Pollster; /* LogShamer happens when we need to find and assign work to a worker job for backups */ IF @Backup = 1 GOTO LogShamer; /* Pollster use happens strictly to check for new databases in sys.databases to place them in a worker queue */ IF @PollDiskForNewDatabases = 1 GOTO DiskPollster; /* Restoregasm Addict happens when we need to find and assign work to a worker job for restores */ IF @Restore = 1 GOTO Restoregasm_Addict; /* Begin Polling section */ /* This section runs in a loop checking for new databases added to the server, or broken backups */ Pollster: IF @Debug = 1 RAISERROR('Beginning Pollster', 0, 1) WITH NOWAIT; IF OBJECT_ID('msdbCentral.dbo.backup_worker') IS NOT NULL BEGIN WHILE @PollForNewDatabases = 1 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. */ 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; 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 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; 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' ); END; --End check for wayward databases /* Wait 1 minute between runs, we don't need to be checking this constantly */ IF @Debug = 1 RAISERROR('Waiting for 1 minute', 0, 1) WITH NOWAIT; WAITFOR DELAY '00:01:00.000'; END TRY BEGIN CATCH 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; WHILE @@TRANCOUNT > 0 ROLLBACK; END CATCH; END; /* 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; /* End of Pollster */ /* Begin DiskPollster */ /* This section runs in a loop checking restore path for new databases added to the server, or broken restores */ DiskPollster: IF @Debug = 1 RAISERROR('Beginning DiskPollster', 0, 1) WITH NOWAIT; IF OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL BEGIN WHILE @PollDiskForNewDatabases = 1 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; /* Look for new non-system databases -- there should probably be additional filters here for accessibility, etc. */ /* 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 */ 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 BEGIN RAISERROR('No rows were returned for that database\path', 0, 1) WITH NOWAIT; END; IF ( SELECT COUNT(*) FROM @FileList AS fl WHERE fl.BackupFile = 'Access is denied.' ) = 1 BEGIN RAISERROR('Access is denied to %s', 16, 1, @restore_path_base) WITH NOWAIT; END; 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 ( 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, @restore_path_base) WITH NOWAIT; END; INSERT msdb.dbo.restore_worker (database_name) SELECT fl.BackupFile FROM @FileList AS fl WHERE fl.BackupFile IS NOT NULL AND fl.BackupFile NOT IN (SELECT name from sys.databases where database_id < 5) 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) */ 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 check for wayward databases /* Wait 1 minute between runs, we don't need to be checking this constantly */ /* 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'; END TRY BEGIN CATCH 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; WHILE @@TRANCOUNT > 0 ROLLBACK; 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; /* Begin LogShamer */ 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; SELECT @changebackuptype = configuration_setting FROM msdbCentral.dbo.backup_configuration c WHERE configuration_name = N'change backup type' AND database_name = N'all'; SELECT @encrypt = configuration_setting FROM msdbCentral.dbo.backup_configuration c WHERE configuration_name = N'encrypt' AND database_name = N'all'; SELECT @encryptionalgorithm = configuration_setting FROM msdbCentral.dbo.backup_configuration c WHERE configuration_name = N'encryptionalgorithm' AND database_name = N'all'; SELECT @servercertificate = configuration_setting FROM msdbCentral.dbo.backup_configuration c WHERE configuration_name = N'servercertificate' AND database_name = N'all'; 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 /* Start loop to take log backups */ BEGIN BEGIN TRY BEGIN TRAN; IF @Debug = 1 RAISERROR('Begin tran to grab a database to back up', 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 = 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; SET @database = NULL; WHILE @@TRANCOUNT > 0 ROLLBACK; END CATCH; /* If we don't find a database to work on, wait for a few seconds */ IF @database IS NULL 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 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 */ 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 /* 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; /* Set @database back to NULL to avoid variable assignment weirdness */ SET @database = NULL; /* 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 backup END CATCH; IF @database IS NOT NULL AND @error_number IS NULL /* If no error, update everything normally */ 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; /* Set @database back to NULL to avoid variable assignment weirdness */ SET @database = NULL; END; -- End update for successful backup END; -- End @Backup WHILE loop END; -- End successful check for backup_worker and subsequent code 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 */ ) AND rw.ignore_database = 0 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 */ 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! */ 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; SET @database = NULL; WHILE @@TRANCOUNT > 0 ROLLBACK; END CATCH; /* If we don't find a database to work on, wait for a few seconds */ IF @database IS NULL 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 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 */ 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 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 */ 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; /* Set @database back to NULL to avoid variable assignment weirdness */ SET @database = NULL; /* 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 no error, update everything normally */ BEGIN IF @Debug = 1 RAISERROR('Error number IS NULL', 0, 1) WITH NOWAIT; /* 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; 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; 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 /* Set @database back to NULL to avoid variable assignment weirdness */ 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 IF OBJECT_ID('dbo.sp_AllNightLog_Setup') IS NULL EXEC ('CREATE PROCEDURE dbo.sp_AllNightLog_Setup AS RETURN 0;'); GO 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, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; BEGIN; SELECT @Version = '3.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; IF @Help = 1 BEGIN PRINT ' /* sp_AllNightLog_Setup from http://FirstResponderKit.org This script sets up a database, tables, rows, and jobs for sp_AllNightLog, including: * 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) 2019 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' ) 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 /* 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; /* Check for tables and stuff */ RAISERROR('Checking for tables in msdbCentral', 0, 1) WITH NOWAIT; 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; /* This section creates tables for restore workers to work off of */ /* In search of msdb */ RAISERROR('Checking for msdb. Yeah, I know...', 0, 1) WITH NOWAIT; IF DATABASEPROPERTYEX('msdb', 'Status') IS NULL BEGIN RAISERROR('YOU HAVE NO MSDB WHY?!', 0, 1) WITH NOWAIT; RETURN; 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; CREATE TABLE msdb.dbo.restore_configuration ( database_name NVARCHAR(256), configuration_name NVARCHAR(512), configuration_description NVARCHAR(512), configuration_setting NVARCHAR(MAX) ); END; ELSE BEGIN RAISERROR('Restore configuration table exists, truncating', 0, 1) WITH NOWAIT; TRUNCATE TABLE msdb.dbo.restore_configuration; 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 BEGIN 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 ); 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; END; /* Add Jobs */ /* Look for our ten second schedule -- all jobs use this to restart themselves if they fail Fun fact: you can add the same schedule name multiple times, so we don't want to just stick it in there */ RAISERROR('Checking for ten second schedule', 0, 1) 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; 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 */ RAISERROR('Checking for pollster job', 0, 1) 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; 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; EXEC msdb.dbo.sp_add_jobserver @job_name = sp_AllNightLog_PollForNewDatabases; RAISERROR('Attaching schedule', 0, 1) WITH NOWAIT; EXEC msdb.dbo.sp_attach_schedule @job_name = sp_AllNightLog_PollForNewDatabases, @schedule_name = ten_seconds; END; /* Look for Restore Pollster job -- this job sets up our watcher for new databases to back up */ RAISERROR('Checking for restore pollster job', 0, 1) WITH NOWAIT; 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 @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; 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; EXEC msdb.dbo.sp_add_jobserver @job_name = sp_AllNightLog_PollDiskForNewDatabases; RAISERROR('Attaching schedule', 0, 1) WITH NOWAIT; EXEC msdb.dbo.sp_attach_schedule @job_name = sp_AllNightLog_PollDiskForNewDatabases, @schedule_name = ten_seconds; END; /* This section creates @Jobs (quantity) of worker jobs to take log backups with They work in a queue It's queuete */ 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[_]%'; 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; RAISERROR(@msg, 0, 1) WITH NOWAIT; WHILE @counter <= @Jobs BEGIN RAISERROR('Setting job name', 0, 1) WITH NOWAIT; 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; EXEC sp_executesql @job_sql; END; /* This section creates @Jobs (quantity) of worker jobs to restore logs with They too work in a queue Like a queue-t 3.14 */ 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[_]%'; 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; RAISERROR(@msg, 0, 1) WITH NOWAIT; WHILE @counter <= @Jobs BEGIN RAISERROR('Setting job name', 0, 1) WITH NOWAIT; 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; 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 @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; EXEC sp_executesql @job_sql; END; RAISERROR('Setup complete!', 0, 1) WITH NOWAIT; END; --End for the Agent job creation END;--End for Database and Table creation END TRY BEGIN CATCH 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; WHILE @@TRANCOUNT > 0 ROLLBACK; END CATCH; END; /* IF @RunSetup = 1 */ RETURN; UpdateConfigs: IF @UpdateSetup = 1 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; BEGIN TRY RESTORE DATABASE [msdbCentral] WITH RECOVERY; END TRY BEGIN CATCH SELECT @error_number = ERROR_NUMBER(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); 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; END CATCH; END /* 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 OBJECT_ID('msdbCentral.dbo.backup_configuration') IS NOT NULL RAISERROR('Found backup config, checking variables...', 0, 1) WITH NOWAIT; BEGIN BEGIN TRY IF @RPOSeconds IS NOT NULL BEGIN RAISERROR('Attempting to update RPO setting', 0, 1) WITH NOWAIT; 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'; END; IF @BackupPath IS NOT NULL BEGIN RAISERROR('Attempting to update Backup Path setting', 0, 1) WITH NOWAIT; UPDATE c SET c.configuration_setting = @BackupPath FROM msdbCentral.dbo.backup_configuration AS c WHERE c.configuration_name = N'log backup path'; END; END TRY BEGIN CATCH 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; END; IF OBJECT_ID('msdb.dbo.restore_configuration') IS NOT NULL RAISERROR('Found restore config, checking variables...', 0, 1) WITH NOWAIT; BEGIN BEGIN TRY EXEC msdb.dbo.sp_update_schedule @name = ten_seconds, @active_start_date = @active_start_date, @active_start_time = 000000; 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 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 @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 @RTOSeconds IS NOT NULL 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'; END; IF @RestorePath IS NOT NULL 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'; END; END TRY BEGIN CATCH SELECT @error_number = ERROR_NUMBER(), @error_severity = ERROR_SEVERITY(), @error_state = ERROR_STATE(); 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; END CATCH; END; RAISERROR('Update complete!', 0, 1) WITH NOWAIT; RETURN; END; --End updates to configuration table 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 , @UsualDBOwner sysname = NULL , @Debug TINYINT = 0 , @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '7.6', @VersionDate = '20190702'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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 | ''XML'' =table output as XML | ''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, 2019. Copyright (c) 2019 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;/* IF @OutputType = 'SCHEMA' */ ELSE 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 ,@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 /* End of declarations for First Responder Kit consistency check:*/ ; 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; /* --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) ); /* 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 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; -- 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 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) 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; /* 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-' 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 (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 (184); /* xp_readerrorlog checking for IFI */ INSERT INTO #SkipChecks (CheckID) VALUES (211); /* xp_regread checking for power saving */ INSERT INTO #SkipChecks (CheckID) VALUES (212); /* xp_regread */ INSERT INTO #SkipChecks (CheckID) VALUES (219); 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 */ 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 */ /* 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 (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 (74); /* TraceFlag On - because Azure Managed Instances go wild and crazy with the trace flags */ 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 (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 #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 ); 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..#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)); 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 ('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 ('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 ('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(MINUTE, (-1. * (@MsSinceWaitsCleared) / 1000. / 60.), 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; IF SERVERPROPERTY('EngineName') <> 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://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://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: - 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, 3)) <> 'HTT' AND @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; 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 backup_start_date) 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 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) 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_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 @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 state <> 1 /* Restoring */ 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 = 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 ) 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://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 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 AND name NOT IN ('CommandLog','SqlServerVersions'); 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 , 'model' 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://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; --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://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://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 , ( ''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; 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 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; 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; IF @UsualDBOwner IS NULL SET @UsualDBOwner = SUSER_SNAME(0x01); 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://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 = 213 ) BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 213) WITH NOWAIT; 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 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 ) AND SERVERPROPERTY('EngineEdition') <> 8 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', '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 ) 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 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, DatabaseName, URL, Details) SELECT 112 AS CheckID, 100 AS Priority, ''Performance'' AS FindingsGroup, ''Change Tracking Enabled'' AS Finding, d.[name], ''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(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; 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 / 128.0 / 1024.) >= 2. THEN CONVERT(DECIMAL(38, 2), (pages_kb / 128.0 / 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, 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)) + 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://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 = 14 AND @ProductVersionMinor < 1000) OR (@ProductVersionMajor = 13 AND @ProductVersionMinor < 4001) OR (@ProductVersionMajor = 12 AND @ProductVersionMinor < 5000) 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); END; 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; END; /* 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; /* 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 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', 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://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') AND SERVERPROPERTY('EngineEdition') <> 8; /* Hekaton is always enabled in Managed Instances per https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/1919 */ 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.state <> 1 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 <> 1 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 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_foreachdb'',''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 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', '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 = 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') 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 */ 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 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; 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, 203) 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%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%' 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, 207) 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, 208) 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, 205) 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, 209) 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, 210) 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, 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 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, 216) 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, 216) 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 @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 [?]; 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://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 [?]; 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://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 @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 [?]; 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://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 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 [?]; 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://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 [?]; 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://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.'' 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 [?]; 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://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 [?]; 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://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 [?]; 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://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 [?]; 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://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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 38, N''?'', 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 = N''?'' 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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 164, N''?'', 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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 39, N''?'', 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 = N''?'' 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 [?]; 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://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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT 47, N''?'', 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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT DISTINCT 48, N''?'', 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 N''?'' 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 [?]; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; INSERT INTO #BlitzResults (CheckID, DatabaseName, Priority, FindingsGroup, Finding, URL, Details) SELECT 56, N''?'', 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 [?]; 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://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 [?]; 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://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 [?]; 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://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%'; 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 (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 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://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 [?]; 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://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 [?]; 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://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 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://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 [?]; 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://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 [?]; 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://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://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 [?]; 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://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; /* 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 [?]; 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://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; /* 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://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. /* 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://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://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. 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 [?]; 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://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(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; 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 threshold 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; /* 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 & NUMA' AS Finding , 'https://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 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] = '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)) 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)) + 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; 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 = 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://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 ) 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 = 211 ) BEGIN IF @Debug IN (1, 2) RAISERROR('Running CheckId [%d].', 0, 1, 211) WITH NOWAIT; DECLARE @outval VARCHAR(36); /* Get power plan if set by group policy [Git Hub Issue #1620] */ EXEC master.sys.xp_regread @rootkey = 'HKEY_LOCAL_MACHINE', @key = 'SOFTWARE\Policies\Microsoft\Power\PowerSettings', @value_name = 'ActivePowerScheme', @value = @outval OUTPUT; IF @outval IS NULL /* If power plan was not set by group policy, get local value [Git Hub Issue #1620]*/ EXEC master.sys.xp_regread @rootkey = 'HKEY_LOCAL_MACHINE', @key = 'SYSTEM\CurrentControlSet\Control\Power\User\PowerSchemes', @value_name = 'ActivePowerScheme', @value = @outval OUTPUT; DECLARE @cpu_speed_mhz int, @cpu_speed_ghz decimal(18,2); EXEC master.sys.xp_regread @rootkey = 'HKEY_LOCAL_MACHINE', @key = 'HARDWARE\DESCRIPTION\System\CentralProcessor\0', @value_name = '~MHz', @value = @cpu_speed_mhz OUTPUT; SELECT @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 ' + CAST(@cpu_speed_ghz as VARCHAR(4)) + 'GHz CPUs, and is in ' + CASE @outval 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' 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; 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://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) */ /* 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 */ /* 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.BrenOzar.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' 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; /* 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 = '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; 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, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS BEGIN SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '3.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; 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. 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) 2019 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.', 16, 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.', 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.', 16, 1) WITH NOWAIT RETURN; END 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 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'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, 16, 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, 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; 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.##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(200), 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, 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, 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, is_spool_expensive BIT, is_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, 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(258) = NULL , @OutputDatabaseName NVARCHAR(258) = NULL , @OutputSchemaName NVARCHAR(258) = NULL , @OutputTableName NVARCHAR(258) = NULL , @ConfigurationDatabaseName NVARCHAR(128) = NULL , @ConfigurationSchemaName NVARCHAR(258) = NULL , @ConfigurationTableName NVARCHAR(258) = 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, @SlowlySearchPlansFor NVARCHAR(4000) = 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, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS BEGIN SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '7.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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) 2019 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", "Spills". 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(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, 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'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(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.'; 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; /*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.##BlitzCacheResults') IS NULL BEGIN CREATE TABLE ##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.##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, 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, 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, is_spool_expensive BIT, is_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, 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), @NoobSaibot BIT = 0; IF @SortOrder = 'sp_BlitzIndex' BEGIN RAISERROR(N'OUTSTANDING!', 0, 1) WITH NOWAIT; SET @SortOrder = 'reads'; SET @NoobSaibot = 1; END 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 SERVERPROPERTY('EngineEdition') IN (5, 6, 8) 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 = LOWER(@SortOrder); 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 ('spill') THEN 'spills' WHEN @SortOrder IN ('avg spill') THEN 'avg spills' WHEN @SortOrder IN ('execution') THEN 'executions' 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', 'spills', 'avg spills', 'all', 'all avg', 'sp_BlitzIndex') 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; DECLARE @VersionShowsAirQuoteActualPlans BIT; 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 AND OBJECT_ID('tempdb..##BlitzCacheResults') IS NULL BEGIN RAISERROR(N'##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 ##BlitzCacheResults WHERE SPID = @@SPID OPTION (RECOMPILE) ; RAISERROR(N'Cleaning up old plans for your SPID', 0, 1) WITH NOWAIT; DELETE ##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; IF OBJECT_ID('tempdb..#index_spool_ugly') IS NOT NULL DROP TABLE #index_spool_ugly; 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) ); 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; 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, StatementStartOffset, StatementEndOffset, 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) ' ; 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 @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 EXISTS (SELECT * FROM sys.all_objects o INNER JOIN sys.all_columns c ON o.object_id = c.object_id WHERE o.name = 'dm_hadr_database_replica_states' AND c.name = 'is_primary_replica') 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 sys.dm_hadr_database_replica_states where is_primary_replica = 0 AND DATABASEPROPERTYEX(DB_NAME(database_id), ''Updateability'') = ''READ_ONLY'')' + @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 ; 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 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 ''%' + @SlowlySearchPlansFor + '%'' ' + @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'spills' THEN N'max_spills' /* 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 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 ; 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, 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,'; 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' COALESCE(deqps.query_plan, qp.query_plan) 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 '; 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, 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 @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(( 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; 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' COALESCE(deqps.query_plan, qp.query_plan) 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 '; 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')) 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(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' WHEN N'spills' THEN N'max_spills' /* 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'spills' THEN N'MaxSpills' /* 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 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; 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 = 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 ##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 = 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 ##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 COUNT(DISTINCT QueryHash) AS distinct_plan_count, COUNT(QueryHash) AS number_of_plans, QueryHash FROM ##BlitzCacheProcs WHERE SPID = @@SPID GROUP BY QueryHash ) AS x WHERE ##BlitzCacheProcs.QueryHash = x.QueryHash 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); /* 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 */ RAISERROR(N'Gathering additional plan level information', 0, 1) WITH NOWAIT; WITH XMLNAMESPACES('http://schemas.microsoft.com/sqlserver/2004/07/showplan' AS p) 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 COUNT(DISTINCT QueryHash) AS distinct_plan_count, COUNT(QueryHash) AS number_of_plans, QueryHash FROM ##BlitzCacheProcs WHERE SPID = @@SPID GROUP BY QueryHash ) AS x WHERE ##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 ##BlitzCacheProcs p JOIN sys.dm_exec_procedure_stats s ON p.SqlHandle = s.sql_handle WHERE QueryType = 'Statement' AND SPID = @@SPID 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, 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); 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) - 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' 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'') 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' 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' 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 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_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' 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 + 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 , 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) ) + ')' 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) ) + ')' 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) ) + ' 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) ) + ' 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 + 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 , 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. Possible reasons for this include dynamic SQL, RECOMPILE hints, and encrypted code.') 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, 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 LastExecutionTime >= 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'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; 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], 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 > 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_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 , 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], 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, [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 LastExecutionTime >= 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'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; 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 ##BlitzCacheProcs WHERE frequent_execution = 1 AND SPID = @@SPID) 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/', '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', '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 ##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 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 ##BlitzCacheProcs WHERE is_cursor = 1 AND SPID = @@SPID) 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.'); 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', '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 ##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', '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 ##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', 'http://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', 'http://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', 'http://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', '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 ##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', '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 ##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', '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 ##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 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 ##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 Index Request', 'http://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', '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 ##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', '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 ##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', '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 ##BlitzCacheProcs WHERE tvf_join = 1 AND SPID = @@SPID) 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/', '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', '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 ##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', '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 ##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', '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 ##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 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 ##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', '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 ##BlitzCacheProcs WHERE unparameterized_query = 1 AND SPID = @@SPID) INSERT INTO ##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 ##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', '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 ##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', '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 ##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 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 ##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', '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 ##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, '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 ##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', '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 ##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', '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 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', '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 ##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, '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 ##BlitzCacheProcs p WHERE p.relop_warnings = 1 AND SPID = @@SPID) 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/', '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, '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 ##BlitzCacheProcs p WHERE p.backwards_scan = 1 AND SPID = @@SPID) INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 38, 200, '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 ##BlitzCacheProcs p WHERE p.forced_index = 1 AND SPID = @@SPID) INSERT INTO ##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 ##BlitzCacheProcs p WHERE p.forced_seek = 1 OR 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', '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, '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 ##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, '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 ##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', '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 ##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, '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 ##BlitzCacheProcs p WHERE p.index_ops >= 5 AND SPID = @@SPID) INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 45, 100, 'Many Indexes Modified', 'Write Queries Are Hitting >= 5 Indexes', '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, 'Plan Confusion', 'Row Level Security is in use', '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, 'Spatial Abuse', 'You hit a 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, '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 ##BlitzCacheProcs p WHERE p.table_dml = 1 AND SPID = @@SPID) INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 49, 150, 'Table DML', 'Tables were created or dropped', '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, '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 ##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, '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 ##BlitzCacheProcs p WHERE p.stale_stats = 1 AND SPID = @@SPID) 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', '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, 'Adaptive joins', 'This is pretty cool -- you''re living in the future.', 'https://www.brentozar.com/blitzcache/adaptive-joins/', 'Joe Sack rules.') ; 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, '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 ##BlitzCacheProcs p WHERE p.is_spool_more_rows = 1 ) 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', '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, 'Potentially bad cardinality estimates', 'Estimated rows are different from average rows by a factor of 10000', 'https://www.brentozar.com/blitzcache/bad-estimates/', '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!', '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 OR (@v = 13 AND @build >= 5026) BEGIN INSERT INTO ##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], '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, 'Row Goals', 'This query had row goals introduced', 'https://www.brentozar.com/go/rowgoals/', 'This 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 Spills', 'This query spills >500mb to tempdb on average', 'https://www.brentozar.com/blitzcache/tempdb-spills/', '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, 'MSTVFs', 'These have many of the same problems scalar UDFs have', '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 ##BlitzCacheProcs p WHERE p.is_mm_join = 1 ) INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 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 ##BlitzCacheProcs p WHERE p.is_nonsargable = 1 ) INSERT INTO ##BlitzCacheResults (SPID, CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (@@SPID, 62, 50, 'Non-SARGable queries', 'Queries may have non-SARGable predicates', '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, 'High Compile Time', 'Queries taking >5 seconds to compile', 'https://www.brentozar.com/blitzcache/high-compilers/', '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, 'High Compile CPU', 'Queries taking >5 seconds of CPU to compile', 'https://www.brentozar.com/blitzcache/high-compilers/', '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, 'High Compile Memory', 'Queries taking 10% of Max Compile Memory', 'https://www.brentozar.com/blitzcache/high-compilers/', 'If you see high RESOURCE_SEMAPHORE_QUERY_COMPILE waits, these may be related'); 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, 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 ##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 ); END; IF @OutputDatabaseName IS NOT NULL AND @OutputSchemaName IS NOT NULL AND @OutputTableName IS NOT NULL 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 @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, PlanHandle VARBINARY(64), SqlHandle VARBINARY(64), SetOptions VARCHAR(MAX), Pattern NVARCHAR(20) ); 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, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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; 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, PlanHandle, SqlHandle, SetOptions, Pattern FROM #bou_allsort ORDER BY Id OPTION(RECOMPILE); '; 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, MinSpills, MaxSpills, TotalSpills, AvgSpills, PlanCreationTime, LastExecutionTime, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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, PlanHandle, SqlHandle, SetOptions ) 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 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; 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, PlanHandle, SqlHandle, SetOptions, Pattern FROM #bou_allsort ORDER BY Id OPTION(RECOMPILE); '; 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_DatabaseName = @DatabaseName, @i_Top = @Top, @i_SkipAnalysis = @SkipAnalysis, @i_OutputDatabaseName = @OutputDatabaseName, @i_OutputSchemaName = @OutputSchemaName, @i_OutputTableName = @OutputTableName; /*End of AllSort section*/ /*Begin code to sort by all*/ OutputResultsToTable: 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; SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), @OutputSchemaName = QUOTENAME(@OutputSchemaName), @OutputTableName = QUOTENAME(@OutputTableName); /* 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(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,' + 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, MinSpills BIGINT, MaxSpills BIGINT, TotalSpills BIGINT, AvgSpills 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''') ' + N'SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;' + '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, MinSpills, MaxSpills, TotalSpills, AvgSpills, 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, MinSpills, MaxSpills, TotalSpills, AvgSpills, QueryPlanCost ' + N' FROM ##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 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 'avg spills' THEN N' AvgSpills' 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; 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 , @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 , @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 TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '7.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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) 2019 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(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 ; /* 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 */ @LineFeed = CHAR(13) + CHAR(10), @StartSampleTime = SYSDATETIMEOFFSET(), @FinishSampleTime = DATEADD(ss, @Seconds, SYSDATETIMEOFFSET()), @FinishSampleTimeWaitFor = DATEADD(ss, @Seconds, GETDATE()), @OurSessionID = @@SPID, @OutputType = UPPER(@OutputType); IF(@OutputType = 'NONE' AND @ExpertMode = 0 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 @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, @CheckDate, @LogMessageCheckID, @LogMessagePriority, @LogMessageFindingsGroup, @LogMessageFinding, @LogMessage, @LogMessageURL)'; EXECUTE sp_executesql @StringToExecute, N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset, @LogMessageCheckID INT, @LogMessagePriority TINYINT, @LogMessageFindingsGroup VARCHAR(50), @LogMessageFinding VARCHAR(200), @LogMessage NVARCHAR(4000), @LogMessageCheckDate DATETIMEOFFSET, @LogMessageURL VARCHAR(200)', @@SERVERNAME, @StartSampleTime, @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 SERVERPROPERTY('Edition') = 'SQL Azure' 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; ELSE IF @Seconds = 0 AND 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()); 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 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(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, ); 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 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 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',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',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',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 ('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 <> 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 ((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 SUBSTRING(@FilterPlansByDatabase, f, t - f) FROM a WHERE SUBSTRING(@FilterPlansByDatabase, f, t - f) IS NOT NULL OPTION (MAXRECURSION 0); END; END; SET @StockWarningHeader = '', @StockDetailsHeader = @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 EXISTS ( SELECT 1/0 FROM ##WaitCategories AS wc WHERE wc.WaitType = x.wait_type AND wc.Ignorable = 0 ) 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) ' + @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_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%' AND s.program_name <> 'SQL Server Log Shipping'; /* 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 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) 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 /* 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);'; 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 - Clients using implicit transactions */ 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 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; /* 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(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('Edition') <> 'SQL Azure' 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 Info - Memory Grants pending - CheckID 39 */ 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 */ DECLARE @MaxWorkspace BIGINT SET @MaxWorkspace = (SELECT CAST(cntr_value AS INT)/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, 'http://www.BrentOzar.com/askbrent/' AS URL FROM sys.dm_exec_query_memory_grants AS Grants; 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 ) 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 > ( deqp.estimate_row_count * 10000 ) THEN 1 WHEN deqp.row_count < ( deqp.estimate_row_count * 10000 ) THEN 1 ELSE 0 END AS estimate_inaccuracy FROM sys.dm_exec_query_profiles AS deqp ) 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 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; /* CheckID 42: Queries in dm_exec_query_profiles showing signs of poor cardinality estimates */ INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) SELECT 42 AS CheckID, 100 AS Priority, ''Query Performance'' AS FindingsGroup, ''Queries with 10000x cardinality misestimations'' AS Findings, ''https://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 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; /* CheckID 43: Queries in dm_exec_query_profiles showing signs of unbalanced parallelism */ INSERT INTO #BlitzFirstResults (CheckID, Priority, FindingsGroup, Finding, URL, Details, HowToStopIt, StartTime, LoginName, NTUserName, ProgramName, HostName, DatabaseID, DatabaseName, QueryText, OpenTransactionCount) SELECT 43 AS CheckID, 100 AS Priority, ''Query Performance'' AS FindingsGroup, ''Queries with 10000x skewed parallelism'' AS Findings, ''https://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 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; '; EXECUTE sp_executesql @StringToExecute; 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. */ 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 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 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 , 'http://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 DESC FOR XML PATH(N''), TYPE ).value(N'.[1]', N'VARCHAR(MAX)'), 1, 1, N'') AS query FROM y ORDER BY y.event_date DESC; /* 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 DATEADD(SECOND,1,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 EXISTS ( SELECT 1/0 FROM ##WaitCategories AS wc WHERE wc.WaitType = x.wait_type AND wc.Ignorable = 0 ) 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'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(@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', '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 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 */ /* Azure Performance - Database is Maxed Out - CheckID 41 */ IF SERVERPROPERTY('Edition') = 'SQL Azure' 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://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); /* 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(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 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 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((CAST(waits2.waits_ms - waits1.waits_ms AS MONEY)) / 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; 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 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; 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; EXEC sp_BlitzCache @OutputDatabaseName = @UnquotedOutputDatabaseName, @OutputSchemaName = @UnquotedOutputSchemaName, @OutputTableName = @OutputTableNameBlitzCache, @CheckDateOverride = @StartSampleTime, @SortOrder = 'all', @SkipAnalysis = 1, @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 + '.' + QUOTENAME(@OutputTableNameBlitzCache) + ' WHERE ServerName = @SrvName AND CheckDate < @CheckDate;'; EXEC sp_executesql @StringToExecute, N'@SrvName NVARCHAR(128), @CheckDate date', @@SERVERNAME, @OutputTableCleanupDate; 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, 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 ' + ' @SrvName, @CheckDate, 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 sp_executesql @StringToExecute, N'@SrvName NVARCHAR(128), @CheckDate datetimeoffset', @@SERVERNAME, @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', @@SERVERNAME, @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, 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, Details, 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', @@SERVERNAME, @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); /* 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 + '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 + ' 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', @@SERVERNAME, @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', @@SERVERNAME, @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', @@SERVERNAME, @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); /* 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 + '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 + ' 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 /* Create the second view */ SET @ObjectFullName = @OutputDatabaseName + N'.' + @OutputSchemaName + N'.' + @OutputTableNamePerfmonStatsActuals_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 + ' ' + @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 + '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 + '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 + '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', @@SERVERNAME, @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', @@SERVERNAME, @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', @@SERVERNAME, @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); /* 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 + '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 + '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', @@SERVERNAME, @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', @@SERVERNAME, @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', @@SERVERNAME, @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 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 @OutputType <> 'NONE' 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 @OutputType <> 'NONE' 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'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 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((CAST(wd2.wait_time_ms - wd1.wait_time_ms AS MONEY)) / 1000.0 / cores.cpu_count / DATEDIFF(ss, wd1.SampleTime, wd2.SampleTime) AS DECIMAL(18,1)) AS [Per Core Per Second], c.[Signal Wait Time (Seconds)], CASE WHEN c.[Wait Time (Seconds)] > 0 THEN CAST(100.*(c.[Signal Wait Time (Seconds)]/c.[Wait Time (Seconds)]) AS NUMERIC(4,1)) ELSE 0 END AS [Percent Signal Waits], (wd2.waiting_tasks_count - wd1.waiting_tasks_count) AS [Number of Waits], CASE WHEN (wd2.waiting_tasks_count - wd1.waiting_tasks_count) > 0 THEN CAST((wd2.wait_time_ms-wd1.wait_time_ms)/ (1.0*(wd2.waiting_tasks_count - wd1.waiting_tasks_count)) AS NUMERIC(12,1)) ELSE 0 END AS [Avg ms Per Wait], N'https://www.sqlskills.com/help/waits/' + LOWER(wd1.wait_type) + '/' AS URL FROM max_batch b JOIN #WaitStats wd2 ON 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, @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 , @Help TINYINT = 0, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 WITH RECOMPILE AS SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '7.6', @VersionDate = '20190702'; SET @OutputType = UPPER(@OutputType); IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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. MIT License Copyright (c) 2019 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); DECLARE @DaysUptimeInsertValue NVARCHAR(256); DECLARE @DatabaseToIgnore NVARCHAR(MAX); 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 = LTRIM(RTRIM(@IgnoreDatabases)); 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(@OutputType = 'NONE') BEGIN IF(@OutputTableName IS NULL OR @OutputSchemaName IS NULL OR @OutputDatabaseName IS NULL) BEGIN RAISERROR('This procedure should be called with a value for @Output* parameters, as @OutputType is set to NONE',12,1); RETURN; END; 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..#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 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 ); 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, 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_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 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 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''), [more_info] AS N'EXEC dbo.sp_BlitzIndex @DatabaseName=' + QUOTENAME([database_name],N'''') + N', @SchemaName=' + QUOTENAME([schema_name],N'''') + N', @TableName=' + QUOTENAME([object_name],N'''') + 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 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 ); 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 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, 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_' + 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=?, 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';' ); 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] NVARCHAR(4000) 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(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 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 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 ); 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 #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 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 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 @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 @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 = @@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 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://FirstResponderKit.org', 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', '', '', '', '' ); 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 insanity 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 */ SELECT @DaysUptime = CAST(DATEDIFF(HOUR, create_date, GETDATE()) / 24. AS NUMERIC (23,2)) FROM sys.databases WHERE database_id = 2; 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 --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'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)) + 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, 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 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 + 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 + N'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, @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 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, os.page_latch_wait_count, os.page_latch_wait_in_ms, os.page_io_latch_wait_count, os.page_io_latch_wait_in_ms, ' + 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, os.page_latch_wait_count, os.page_latch_wait_in_ms, os.page_io_latch_wait_count, os.page_io_latch_wait_in_ms, ' + 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, page_latch_wait_count, page_latch_wait_in_ms, page_io_latch_wait_count, page_io_latch_wait_in_ms, 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, @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 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, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; 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; 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 @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(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName 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(''.'', ''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; 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, @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(N' + QUOTENAME(@DatabaseName,'''') + N') AS [database_id], @i_DatabaseName 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 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; 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, @params = N'@i_DatabaseName NVARCHAR(128)', @i_DatabaseName = @DatabaseName; END; END; 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; 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 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; 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; 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);' 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; 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('.', '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 + 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('.', '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 + 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('.', '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], 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, 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, 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),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 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 ( /* 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=?, DATA_COMPRESSION=?' + 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 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; 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 ); /*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; --SELECT '#CheckConstraints' AS table_name, * FROM #CheckConstraints; --SELECT '#FilteredIndexes' AS table_name, * FROM #FilteredIndexes; /*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, 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, 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, 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] 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; /* 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'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], 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 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 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, 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 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 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 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 ' + 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) 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 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 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 4, [database_name], 8 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 ' + 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) 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 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 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 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; INSERT #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 ) GROUP BY db_schema_object_name, [i].[database_name] HAVING COUNT(*) >= CASE WHEN (@GetAllDatabases = 1 OR @Mode = 0) THEN 21 ELSE 7 END 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 29 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 /*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()) AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 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'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 AND NOT (@GetAllDatabases = 1 OR @Mode = 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, 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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'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 ) AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 ); 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'Forgetful Indexes' AS findings_group, N'Filter Columns Not In Index Definition' AS finding, [database_name] AS [Database Name], N'http://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 ); ---------------------------------------- --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', 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'Self Loathing Indexes' AS findings_group, N'Heaps with forwarded records' AS finding, [database_name] AS [Database Name], N'http://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 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'Self Loathing Indexes' AS findings_group, N'Heaps with deletes' AS finding, [database_name] AS [Database Name], N'http://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 ); 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 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'Index Hoarder' 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'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 ); 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 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' 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 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, 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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%' AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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) 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, 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 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'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 OPTION ( RECOMPILE ); 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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, 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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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, '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 AND NOT (@GetAllDatabases = 1 OR @Mode = 0) 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 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, '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 AND NOT (@GetAllDatabases = 1 OR @Mode = 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', 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 WHERE NOT (@GetAllDatabases = 1 OR @Mode = 0) OPTION ( RECOMPILE ); ---------------------------------------- --Check Constraint Info: Check_id 120-129 ---------------------------------------- RAISERROR(N'check_id 120: 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 99 AS check_id, 50 AS Priority, 'Obsessive Constraintive' AS findings_group, 'Serial Forcer' AS finding, cc.database_name, '' 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 ); 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.', @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 new 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 (@Mode = 0) 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] 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, 2, 11, 12, 13, 22, 34, 43, 47, 48, 50, 65, 68, 73, 99 ) ORDER BY br.Priority ASC, br.check_id ASC, br.blitz_result_id ASC, br.findings_group ASC OPTION (RECOMPILE); END; END; ELSE IF (@Mode = 4) 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] 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 @Mode=0 or 4 (diagnose)*/ ELSE IF (@Mode=1) /*Summarize*/ BEGIN --This mode is to give some overall stats on the database. 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 @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; 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(); 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), [Drop_Tsql] NVARCHAR(4000), [Create_Tsql] NVARCHAR(4000), [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), [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, [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 */ 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], [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_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], [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], [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.[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.[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}], 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.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.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_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.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.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.[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.[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] 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 ORDER BY [Database Name], [Schema Name], [Object Name], [Index ID] OPTION (RECOMPILE); END; END; /* End @Mode=2 (index detail)*/ ELSE IF (@Mode=3) /*Missing index Detail*/ 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 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, @DaysUptimeInsertValue, 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; 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 @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, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 ) WITH RECOMPILE AS BEGIN SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '2.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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. Known limitations of this version: - Only 2012+ is supported (2008 and 2008R2 are kaput in 2019, so I''m not putting time into them) - 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. - Your mom. Unknown limitations of this version: - None. (If we knew them, they would be known. Duh.) MIT License Copyright (c) 2019 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 ((SELECT SERVERPROPERTY ('EDITION')) = 'SQL Azure' AND LOWER(@EventSessionPath) NOT LIKE 'http%') BEGIN RAISERROR( 'The default storage path doesn''t work in Azure SQLDB/Managed instances. You need to use an Azure storage account, and the path has to look like this: https://StorageAccount.blob.core.windows.net/Container/FileName.xel', 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) ); DECLARE @d VARCHAR(40), @StringToExecute NVARCHAR(4000); CREATE TABLE #t (id INT NOT NULL); /* WITH ROWCOUNT doesn't work on Amazon RDS - see: https://github.com/BrentOzarULTD/SQL-Server-First-Responder-Kit/issues/2037 */ 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 NULL UPDATE STATISTICS #t WITH ROWCOUNT = 100000000, PAGECOUNT = 100000000; /*Grab the initial set of XML to parse*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Grab the initial set of XML to parse at %s', 0, 1, @d) WITH NOWAIT; 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 ) ISNULL(xml.deadlock_xml, '') AS deadlock_xml INTO #deadlock_data FROM xml LEFT JOIN #t AS t ON 1 = 1 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') DESC OPTION ( RECOMPILE ); /*Parse process and input buffer XML*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Parse process and input buffer XML %s', 0, 1, @d) WITH NOWAIT; SELECT q.event_date, q.victim_id, q.deadlock_graph, q.id, q.database_id, 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.transaction_count, q.client_app, q.host_name, q.login_name, q.isolation_level, q.process_xml, ISNULL(ca2.ib.query('.'), '') AS input_buffer INTO #deadlock_process FROM ( SELECT dd.deadlock_xml, dd.event_date, dd.victim_id, dd.deadlock_graph, ca.dp.value('@id', 'NVARCHAR(256)') AS id, ca.dp.value('@currentdb', 'BIGINT') AS database_id, ca.dp.value('@priority', 'SMALLINT') AS priority, 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, ISNULL(ca.dp.query('.'), '') AS process_xml FROM ( SELECT d1.deadlock_xml, d1.deadlock_xml.value('(event/@timestamp)[1]', 'DATETIME2') AS event_date, d1.deadlock_xml.value('(//deadlock/victim-list/victimProcess/@id)[1]', 'NVARCHAR(256)') AS victim_id, d1.deadlock_xml.query('/event/data/value/deadlock') AS deadlock_graph FROM #deadlock_data AS d1 ) AS dd CROSS APPLY dd.deadlock_xml.nodes('//deadlock/process-list/process') AS ca(dp) 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) ) AS q CROSS APPLY q.deadlock_xml.nodes('//deadlock/process-list/process/inputbuf') AS ca2(ib); /*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, 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*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Grab the full resource list %s', 0, 1, @d) WITH NOWAIT; 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, ISNULL(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 ); /*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, ca.object_name, ca.lock_mode, ca.index_name, 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 ( 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, ca.dr.value('@indexname', 'NVARCHAR(256)') AS index_name, ca.dr.query('.') AS dr FROM #deadlock_resource AS dr CROSS APPLY dr.resource_xml.nodes('//resource-list/objectlock') 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) WHERE (ca.object_name = @ObjectName OR @ObjectName IS NULL) OPTION ( RECOMPILE ); /*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, ca.object_name, ca.lock_mode, ca.index_name, 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 ( 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, ca.dr.value('@indexname', 'NVARCHAR(256)') AS index_name, ca.dr.query('.') AS dr FROM #deadlock_resource AS dr CROSS APPLY dr.resource_xml.nodes('//resource-list/pagelock') 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 ); /*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, ca.object_name, ca.lock_mode, ca.index_name, 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 ( 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, ca.dr.value('@indexname', 'NVARCHAR(256)') AS index_name, ca.dr.query('.') AS dr FROM #deadlock_resource AS dr CROSS APPLY dr.resource_xml.nodes('//resource-list/keylock') 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 ); /*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, ca.object_name, ca.lock_mode, ca.index_name, 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 ( 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, ca.dr.value('@indexname', 'NVARCHAR(256)') AS index_name, ca.dr.query('.') AS dr FROM #deadlock_resource AS dr CROSS APPLY dr.resource_xml.nodes('//resource-list/ridlock') 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 ); /*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, ca.object_name, ca.lock_mode, ca.index_name, 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 ( 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, ca.dr.value('@indexname', 'NVARCHAR(256)') AS index_name, ca.dr.query('.') AS dr FROM #deadlock_resource AS dr CROSS APPLY dr.resource_xml.nodes('//resource-list/rowgrouplock') 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 ); /*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, w.l.value('@id', 'NVARCHAR(256)') AS waiter_id, o.l.value('@id', 'NVARCHAR(256)') AS owner_id INTO #deadlock_resource_parallel FROM ( SELECT dr.event_date, ca.dr.value('@id', 'NVARCHAR(256)') AS id, ca.dr.value('@WaitType', 'NVARCHAR(256)') AS wait_type, ca.dr.value('@nodeId', 'BIGINT') AS node_id, /* These columns are in 2017 CU5 ONLY */ ca.dr.value('@waiterType', 'NVARCHAR(256)') AS waiter_type, ca.dr.value('@ownerActivity', 'NVARCHAR(256)') AS owner_activity, ca.dr.value('@waiterActivity', 'NVARCHAR(256)') AS waiter_activity, ca.dr.value('@merging', 'NVARCHAR(256)') AS merging, ca.dr.value('@spilling', 'NVARCHAR(256)') AS spilling, ca.dr.value('@waitingToClose', 'NVARCHAR(256)') AS waiting_to_close, /* */ ca.dr.query('.') AS dr 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 ); /*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 *, ROW_NUMBER() OVER ( PARTITION BY drp.owner_id, drp.waiter_id ORDER BY drp.event_date ) AS rn FROM #deadlock_resource_parallel AS drp ) DELETE FROM c WHERE c.rn > 1; /*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 ); /*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*/ 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('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 ); /*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 *, CONVERT(UNIQUEIDENTIFIER, CONVERT(XML, '').value('xs:hexBinary(substring(sql:column("x.job_id"), 0) )', 'BINARY(16)') ) AS job_id_guid INTO #agent_job FROM ( SELECT dp.event_date, dp.victim_id, dp.id, dp.database_id, dp.client_app, SUBSTRING(dp.client_app, CHARINDEX('0x', dp.client_app) + LEN('0x'), 32 ) AS job_id, SUBSTRING(dp.client_app, CHARINDEX(': Step ', dp.client_app) + LEN(': Step '), CHARINDEX(')', dp.client_app, CHARINDEX(': Step ', dp.client_app)) - (CHARINDEX(': Step ', dp.client_app) + LEN(': Step ')) ) AS step_id FROM #deadlock_process AS dp WHERE dp.client_app LIKE 'SQLAgent - %' ) AS x; ALTER TABLE #agent_job ADD job_name NVARCHAR(256), step_name NVARCHAR(256); IF SERVERPROPERTY('EngineEdition') NOT IN (5, 6) /* Azure SQL DB doesn't support querying jobs */ 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;'; EXEC(@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; /*Begin checks based on parsed values*/ /*Check 1 is deadlocks by database*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 1 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( 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 WHERE 1 = 1 AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id) OPTION ( RECOMPILE ); /*Check 2 is deadlocks by object*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 2 objects %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, object_name, finding_group, finding ) SELECT 2 AS check_id, ISNULL(DB_NAME(dow.database_id), 'UNKNOWN') AS database_name, ISNULL(dow.object_name, 'UNKNOWN') AS object_name, 'Total object deadlocks' AS finding_group, 'This object was involved in ' + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.event_date)) + ' deadlock(s).' FROM #deadlock_owner_waiter AS dow WHERE 1 = 1 AND (DB_NAME(dow.database_id) = @DatabaseName 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 DB_NAME(dow.database_id), dow.object_name OPTION ( RECOMPILE ); /*Check 2 continuation, number of locks per index*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 2 indexes %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, object_name, finding_group, finding ) SELECT 2 AS check_id, ISNULL(DB_NAME(dow.database_id), 'UNKNOWN') AS database_name, dow.index_name AS index_name, 'Total index deadlocks' AS finding_group, 'This index was involved in ' + CONVERT(NVARCHAR(20), COUNT_BIG(DISTINCT dow.event_date)) + ' deadlock(s).' FROM #deadlock_owner_waiter AS dow WHERE 1 = 1 AND (DB_NAME(dow.database_id) = @DatabaseName 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.index_name IS NOT NULL GROUP BY DB_NAME(dow.database_id), dow.index_name OPTION ( RECOMPILE ); /*Check 3 looks for Serializable locking*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 3 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( 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%' AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id) OPTION ( RECOMPILE ); /*Check 4 looks for Repeatable Read locking*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 4 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( 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%' AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id) OPTION ( RECOMPILE ); /*Check 5 breaks down app, host, and login information*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 5 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( 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 WHERE 1 = 1 AND (DB_NAME(dp.database_id) = @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 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.)*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 6 %s', 0, 1, @d) WITH NOWAIT; 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 WHERE 1 = 1 AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id), SUBSTRING(dp.wait_resource, 1, CHARINDEX(':', dp.wait_resource) - 1), dow.object_name ) INSERT #deadlock_findings WITH (TABLOCKX) ( 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*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 7 part 1 %s', 0, 1, @d) WITH NOWAIT; 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 WITH (TABLOCKX) ( check_id, database_name, object_name, finding_group, finding ) SELECT DISTINCT 7 AS check_id, ISNULL(DB_NAME(dow.database_id), 'UNKNOWN') 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 = ' + ds.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 WHERE 1 = 1 AND (DB_NAME(dow.database_id) = @DatabaseName 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 ); IF @ProductVersionMajor >= 13 BEGIN SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 7 part 2 %s', 0, 1, @d) WITH NOWAIT; 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 WITH (TABLOCKX) ( 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' AND (DB_NAME(dow.database_id) = @DatabaseName 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 ); END; /*Check 8 gives you stored proc deadlock counts*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 8 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( 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' AND (ds.proc_name = @StoredProcName OR @StoredProcName IS NULL) AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id), ds.proc_name OPTION(RECOMPILE); /*Check 9 gives you more info queries for sp_BlitzIndex */ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 9 %s', 0, 1, @d) WITH NOWAIT; 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 WHERE 1 = 1 AND (DB_NAME(dow.database_id) = @DatabaseName 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 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*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 10 %s', 0, 1, @d) WITH NOWAIT; 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 WHERE 1 = 1 AND (DB_NAME(dow.database_id) = @DatabaseName 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 (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 PARSENAME(dow.object_name, 3), dow.object_name ) INSERT #deadlock_findings WITH (TABLOCKX) ( 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*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 11 %s', 0, 1, @d) WITH NOWAIT; 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 WHERE 1 = 1 AND (DB_NAME(dp.database_id) = @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 DB_NAME(dp.database_id) ) INSERT #deadlock_findings WITH (TABLOCKX) ( 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 ); /*Check 12 gets total deadlock wait time for SQL Agent*/ SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Check 12 %s', 0, 1, @d) WITH NOWAIT; INSERT #deadlock_findings WITH (TABLOCKX) ( check_id, database_name, object_name, finding_group, finding ) SELECT 12, DB_NAME(aj.database_id), 'SQLAgent - Job: ' + aj.job_name + ' Step: ' + aj.step_name, 'Agent Job Deadlocks', RTRIM(COUNT(*)) + ' deadlocks from this Agent Job and Step' FROM #agent_job AS aj GROUP BY DB_NAME(aj.database_id), aj.job_name, aj.step_name; /*Thank you goodnight*/ INSERT #deadlock_findings WITH (TABLOCKX) ( 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*/ /*Break in case of emergency*/ --CREATE CLUSTERED INDEX cx_whatever ON #deadlock_process (event_date, id); --CREATE CLUSTERED INDEX cx_whatever ON #deadlock_resource_parallel (event_date, owner_id); --CREATE CLUSTERED INDEX cx_whatever ON #deadlock_owner_waiter (event_date, owner_id, waiter_id); SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Results 1 %s', 0, 1, @d) WITH NOWAIT; WITH deadlocks AS ( SELECT N'Regular Deadlock' AS deadlock_type, dp.event_date, dp.id, dp.victim_id, dp.database_id, dp.priority, dp.log_used, dp.wait_resource COLLATE DATABASE_DEFAULT AS wait_resource, CONVERT( XML, STUFF(( SELECT DISTINCT NCHAR(10) + N' ' + ISNULL(c.object_name, N'') + N' ' COLLATE DATABASE_DEFAULT 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, NULL AS owner_waiter_type, NULL AS owner_activity, NULL AS owner_waiter_activity, NULL AS owner_merging, NULL AS owner_spilling, NULL AS owner_waiting_to_close, ISNULL(dp.waiter_mode, '-') AS waiter_mode, NULL AS waiter_waiter_type, NULL AS waiter_owner_activity, NULL AS waiter_waiter_activity, NULL AS waiter_merging, NULL AS waiter_spilling, NULL AS waiter_waiting_to_close, dp.deadlock_graph FROM #deadlock_process AS dp WHERE dp.victim_id IS NOT NULL UNION ALL SELECT N'Parallel Deadlock' AS deadlock_type, dp.event_date, dp.id, dp.victim_id, dp.database_id, dp.priority, dp.log_used, dp.wait_resource COLLATE DATABASE_DEFAULT, CONVERT(XML, N'parallel_deadlock' COLLATE DATABASE_DEFAULT) 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, NULL AS is_victim, cao.wait_type COLLATE DATABASE_DEFAULT AS owner_mode, cao.waiter_type AS owner_waiter_type, cao.owner_activity AS owner_activity, cao.waiter_activity AS owner_waiter_activity, cao.merging AS owner_merging, cao.spilling AS owner_spilling, cao.waiting_to_close AS owner_waiting_to_close, caw.wait_type COLLATE DATABASE_DEFAULT AS waiter_mode, caw.waiter_type AS waiter_waiter_type, caw.owner_activity AS waiter_owner_activity, caw.waiter_activity AS waiter_waiter_activity, caw.merging AS waiter_merging, caw.spilling AS waiter_spilling, caw.waiting_to_close AS waiter_waiting_to_close, dp.deadlock_graph FROM #deadlock_process AS dp CROSS APPLY (SELECT TOP 1 * FROM #deadlock_resource_parallel AS drp WHERE drp.owner_id = dp.id AND drp.wait_type = 'e_waitPipeNewRow' ORDER BY drp.event_date) AS cao CROSS APPLY (SELECT TOP 1 * FROM #deadlock_resource_parallel AS drp WHERE drp.owner_id = dp.id AND drp.wait_type = 'e_waitPipeGetRow' ORDER BY drp.event_date) AS caw WHERE dp.victim_id IS NULL AND dp.login_name IS NOT NULL) SELECT d.deadlock_type, 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'') 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.priority, d.log_used, d.last_tran_started, d.last_batch_started, d.last_batch_completed, d.transaction_name, /*These columns will be NULL for regular (non-parallel) deadlocks*/ 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, d.deadlock_graph FROM deadlocks AS d WHERE d.dn = 1 AND d.en < 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) LIKE '%' + @ObjectName + '%' 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) ORDER BY d.event_date, is_victim DESC OPTION ( RECOMPILE ); SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('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 OPTION ( RECOMPILE ); SET @d = CONVERT(VARCHAR(40), GETDATE(), 109); RAISERROR('Done %s', 0, 1, @d) WITH NOWAIT; 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_resource_parallel' AS table_name, * FROM #deadlock_resource_parallel AS drp 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')) NOT IN (5,8) 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, @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 TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '3.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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); /*Check if this is Azure first*/ IF (SELECT 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 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 MIT License Copyright (c) 2019 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')) NOT IN (5,8) 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, '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; 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, 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) ); /* 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(258), 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(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(258), batch_sql_handle VARBINARY(64), query_hash BINARY(8), query_parameterization_type_desc NVARCHAR(258), 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), 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) ); /* 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(258), 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(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, /*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(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, 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, 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, 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(258), 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, 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' 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, is_cursor BIT 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 BIGINT, sampling_percent DECIMAL(38, 2), [statistics] NVARCHAR(258), [table] NVARCHAR(258), [schema] NVARCHAR(258), [database] NVARCHAR(258), 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(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) 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(1000), 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; 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, INDEX mix_ix_ids CLUSTERED (sql_handle, query_hash) ); 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, INDEX mis_ix_ids CLUSTERED (sql_handle, query_hash) ); 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, INDEX miu_ix_ids CLUSTERED (sql_handle, query_hash) ); 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), INDEX mid_ix_ids CLUSTERED (sql_handle, query_hash) ); 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(MAX), inequality NVARCHAR(MAX), [include] NVARCHAR(MAX), 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 + 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') 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'*/', 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 --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, 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(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, 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 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_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, @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, ''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 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; 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*/ 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, ''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 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; 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*/ 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, ''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 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; 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*/ 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, ''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 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; 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*/ 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, ''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 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; 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*/ 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, ''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 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; 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*/ 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, ''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 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, ''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 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; 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_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 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, ''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 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; 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; /* 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 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 */ 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; /* 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 OPTION(RECOMPILE); 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 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 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); 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 */ 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*/ /*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, 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 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, 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 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); 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.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); 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) 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); END; /*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); 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 ( 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); 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 = 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 ( 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); 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 b SET b.key_lookup_cost = x.key_lookup_cost FROM #working_warnings b JOIN ( SELECT r.sql_handle, 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); 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, 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); 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 sort_cost = y.max_sort_cost FROM #working_warnings b JOIN ( 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 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 #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[.="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 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); 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 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); 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); 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.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); 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 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); END; 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); 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 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); 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 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); 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 ) , 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('@EstimateRebinds', 'FLOAT') AS estimated_rebinds 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 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); 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 and 2016 SP2 specfic 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', '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.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); 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 ( 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; 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; 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 = 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 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'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 ( 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; 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 ); 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(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); 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(4000)') 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 -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, 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); RAISERROR(N'Updating variables 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.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); RAISERROR(N'Inserting variables for other procs', 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 WHERE NOT EXISTS ( SELECT * FROM #stored_proc_info AS sp 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); 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%' 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.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, (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 @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**') 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.sql_handle = spi2.sql_handle 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.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 for procs', 0, 1) WITH NOWAIT; WITH precheck AS ( SELECT spi.sql_handle, spi.proc_name, (SELECT set_options + @cr + @lf + @cr + @lf + 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 @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'') 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, 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'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 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 OR CONVERT(NVARCHAR(MAX), b.cached_execution_parameters) = N'' THEN N'' ELSE b.cached_execution_parameters END FROM #working_warnings AS b OPTION (RECOMPILE); /*End implicit conversion and parameter info*/ /*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 ) 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 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' 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(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.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(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.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(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); 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 DISTINCT mip.query_hash, mip.sql_handle, 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); 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 /*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_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, 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 '; 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 + 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 + 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); 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 = 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 + 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 <> 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 + 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) END 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, 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, 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, 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, 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, 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, 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, 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, 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_cursor = 1 AND is_cursor_dynamic = 1 ) INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES (4, 200, 'Cursors', 'Dynamic Cursors', 'http://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', 'http://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 ) 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 p WHERE busy_loops = 1 ) INSERT INTO #warning_results (CheckID, Priority, FindingsGroup, Finding, URL, Details) VALUES ( 16, 100, 'Performance', 'Busy Loops', '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 #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', '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 #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', '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 #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', '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 #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', 'https://www.brentozar.com/blitzcache/spatial-indexes/', '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', 'https://www.brentozar.com/blitzcache/index-dml/', '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', 'https://www.brentozar.com/blitzcache/table-dml/', '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', '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 #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', '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 #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', 'https://www.brentozar.com/blitzcache/stale-statistics/', '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.', 'https://www.brentozar.com/blitzcache/adaptive-joins/', '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.', 'https://www.brentozar.com/blitzcache/eager-index-spools/', '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', 'https://www.brentozar.com/blitzcache/eager-index-spools/', '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', 'https://www.brentozar.com/blitzcache/bad-estimates/', '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', 'http://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_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', '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 #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 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_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, '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.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)) ; /* 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, 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 ' 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 ), /*averages*/ duration_worst AS ( 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 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 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 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 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 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 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 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 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 ) /*averages*/ SELECT 1002, 255, 'Worsts', 'Worst Avg Duration', 'N/A', duration_worst.msg FROM duration_worst UNION ALL SELECT 1002, 255, 'Worsts', 'Worst Avg CPU', 'N/A', cpu_worst.msg FROM cpu_worst UNION ALL 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 Avg Physical Reads', 'N/A', physical_reads_worst.msg FROM physical_reads_worst UNION ALL 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 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 Avg Log Bytes', 'N/A', logbytes_worst.msg FROM logbytes_worst UNION ALL 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); 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, FindingsGroup, Finding, 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, @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 , @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 AS BEGIN SET NOCOUNT ON; SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT @Version = '7.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; 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. - 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) 2019 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 ,@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' ,@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') BEGIN SET @QueryStatsXMLselect = N' CAST(COALESCE(qs_live.query_plan, '''') AS XML) AS live_query_plan , '; SET @QueryStatsXMLSQL = N'OUTER APPLY sys.dm_exec_query_statistics_xml(s.session_id) qs_live'; END ELSE BEGIN SET @QueryStatsXMLselect = N' NULL AS live_query_plan , '; SET @QueryStatsXMLSQL = N' '; END SELECT @OutputDatabaseName = QUOTENAME(@OutputDatabaseName), @OutputSchemaName = QUOTENAME(@OutputSchemaName), @OutputTableName = QUOTENAME(@OutputTableName); 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'( 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, [query_plan] [xml] NULL, [live_query_plan] [xml] NULL, [query_cost] [float] NULL, [status] [nvarchar](30) NOT NULL, [wait_info] [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, 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); /* 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; END SELECT @BlockingCheck = 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 @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'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 , 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 , s.program_name ' 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.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 /* 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(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) * 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' 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 ,' + @QueryStatsXMLselect +' 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' NULL AS top_session_waits ,' 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 , 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 , s.program_name ' 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.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 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 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) * 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 ' + @QueryStatsXMLSQL + N' 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 >= 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 2 DESC; '; 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] ,[database_name] ,[query_text] ,[query_plan]' + CASE WHEN @ProductVersionMajor >= 11 THEN N',[live_query_plan]' ELSE N'' END + N' ,[query_cost] ,[status] ,[wait_info]' + CASE WHEN @ProductVersionMajor >= 11 THEN N',[top_session_waits]' ELSE N'' END + N' ,[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] ,[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, SYSDATETIMEOFFSET() AS CheckDate , ' + @StringToExecute; END ELSE SET @StringToExecute = @BlockingCheck + N' SELECT GETDATE() AS run_date , ' + @StringToExecute; IF @Debug = 1 BEGIN PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 0, 8000)) PRINT CONVERT(VARCHAR(8000), SUBSTRING(@StringToExecute, 8000, 16000)) END EXEC(@StringToExecute); 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(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, @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, @Execute CHAR(1) = Y, @Debug INT = 0, @Help BIT = 0, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 AS SET NOCOUNT ON; /*Versioning details*/ SELECT @Version = '7.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; 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) 2019 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 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 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 @MajorVersion < 10 BEGIN RAISERROR('Sorry, DatabaseRestore doesn''t work on versions of SQL prior to 2008.', 15, 1); RETURN; 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 @RestoreDatabaseID smallint; --Holds DB_ID of @RestoreDatabaseName DECLARE @FileListSimple TABLE ( BackupFile NVARCHAR(255) NOT NULL, depth int NOT NULL, [file] int NOT NULL ); DECLARE @FileList TABLE ( BackupFile NVARCHAR(255) NULL ); 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), -- -- Seq added to retain order by -- Seq INT NOT NULL IDENTITY(1, 1) ); /* Correct paths in case people forget a final "\" */ /*Full*/ 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 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)) <> '\' --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)) <> '\' --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)) <> '\' --Has to end in a '\' BEGIN IF @Execute = 'Y' OR @Debug = 1 RAISERROR('Fixing @MoveDataDrive 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)) <> '\' --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; /*Standby Undo File*/ 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 BEGIN SET @RestoreDatabaseName = @Database; END; SET @RestoreDatabaseID = DB_ID(@RestoreDatabaseName); SET @RestoreDatabaseName = QUOTENAME(@RestoreDatabaseName); --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'; 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 @BackupPathFull IS NOT NULL BEGIN IF @SimpleFolderEnumeration = 1 BEGIN -- Get list of files INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @BackupPathFull, 1, 1; INSERT @FileList (BackupFile) SELECT BackupFile FROM @FileListSimple; END ELSE BEGIN SET @cmd = N'DIR /b "' + @BackupPathFull + N'"'; IF @Debug = 1 BEGIN IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathFull'; PRINT @cmd; END; INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; END; IF @Debug = 1 BEGIN SELECT 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, @BackupPathFull) 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, @BackupPathFull) 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, @BackupPathFull) 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, @BackupPathFull) 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, @BackupPathFull) WITH NOWAIT; RETURN; END; END; /*End folder sanity check*/ -- 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); /* 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 BackupFile INTO #SplitBackups 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. Olla only supports up to 64 file split. -- 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}', @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); IF @Debug = 1 BEGIN SELECT '#FileListParameters' AS table_name, * FROM #FileListParameters SELECT '#SplitBackups' AS table_name, * FROM #SplitBackups END --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); 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 @MoveFiles = 1 BEGIN IF @Execute = 'Y' RAISERROR('@MoveFiles = 1, adjusting paths', 0, 1) WITH NOWAIT; WITH Files AS ( SELECT CASE WHEN Type = 'D' THEN @MoveDataDrive WHEN Type = 'L' THEN @MoveLogDrive WHEN Type = 'S' THEN @MoveFilestreamDrive 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 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 3 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' EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'ALTER DATABASE SINGLE_USER', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; 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] @Command = @sql, @CommandType = 'KILL', @Mode = 1, @DatabaseName = @Database, @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] @Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; 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 #SplitBackups) > 0 BEGIN RAISERROR('Split backups found', 0, 1) WITH NOWAIT; SELECT @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM ' + STUFF( (SELECT CHAR(10) + ',DISK=''' + @BackupPathFull + BackupFile + '''' FROM #SplitBackups ORDER BY BackupFile FOR XML PATH('')), 1, 2, '') + N' WITH NORECOVERY, REPLACE' + @MoveOption + NCHAR(13); END; ELSE BEGIN SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathFull + @LastFullBackup + N''' WITH NORECOVERY, REPLACE' + @MoveOption + NCHAR(13); 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 BEGIN SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathFull + @LastFullBackup + N''' WITH REPLACE' + @MoveOption + N' , STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13); 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] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @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(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 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; IF @BackupPathFull IS NULL AND @ContinueLogs = 1 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; IF @BackupPathDiff IS NOT NULL BEGIN DELETE FROM @FileList; DELETE FROM @FileListSimple; IF @SimpleFolderEnumeration = 1 BEGIN -- Get list of files INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @BackupPathDiff, 1, 1; INSERT @FileList (BackupFile) SELECT BackupFile FROM @FileListSimple; END ELSE BEGIN SET @cmd = N'DIR /b "' + @BackupPathDiff + N'"'; IF @Debug = 1 BEGIN IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathDiff'; PRINT @cmd; END; INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; END; IF @Debug = 1 BEGIN SELECT BackupFile FROM @FileList; 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, @BackupPathDiff) 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, @BackupPathDiff) 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, @BackupPathDiff) WITH NOWAIT; RETURN; END; 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); --No file = no backup to restore SET @LastDiffBackupDateTime = REPLACE(LEFT(RIGHT(@LastDiffBackup, 19),15), '_', ''); IF @RestoreDiff = 1 AND @BackupDateTime < @LastDiffBackupDateTime BEGIN SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH NORECOVERY' + NCHAR(13); 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 SET @sql = N'RESTORE DATABASE ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathDiff + @LastDiffBackup + N''' WITH STANDBY = ''' + @StandbyUndoPath + @Database + 'Undo.ldf''' + NCHAR(13); 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] @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); IF @Debug = 1 BEGIN SELECT '#Headers' AS table_name, @LastDiffBackup AS DiffbackupFile, * 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; IF @DiffLastLSN IS NULL BEGIN SET @DiffLastLSN=@FullLastLSN END END IF @BackupPathLog IS NOT NULL BEGIN DELETE FROM @FileList; DELETE FROM @FileListSimple; IF @SimpleFolderEnumeration = 1 BEGIN -- Get list of files INSERT INTO @FileListSimple (BackupFile, depth, [file]) EXEC master.sys.xp_dirtree @BackupPathLog, 1, 1; INSERT @FileList (BackupFile) SELECT BackupFile FROM @FileListSimple; END ELSE BEGIN SET @cmd = N'DIR /b "' + @BackupPathLog + N'"'; IF @Debug = 1 BEGIN IF @cmd IS NULL PRINT '@cmd is NULL for @BackupPathLog'; PRINT @cmd; END; INSERT INTO @FileList (BackupFile) EXEC master.sys.xp_cmdshell @cmd; 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, @BackupPathLog) 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, @BackupPathLog) 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, @BackupPathLog) 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, @BackupPathLog) 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, @BackupPathLog) WITH NOWAIT; RETURN; END; END; /*End folder sanity check*/ IF (@OnlyLogsAfter IS NOT NULL) BEGIN 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(LEFT(RIGHT(fl.BackupFile, 19), 15),'_','') < @OnlyLogsAfter 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 (@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 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) AND ((@ContinueLogs = 1 AND REPLACE(LEFT(RIGHT(BackupFile, 19), 15),'_','') <= @StopAt) 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; 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; 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 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; 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; SET @sql = N'RESTORE LOG ' + @RestoreDatabaseName + N' FROM DISK = ''' + @BackupPathLog + @BackupFile + N''' WITH ' + @LogRecoveryOption + NCHAR(13); IF @Debug = 1 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for RESTORE LOG: @RestoreDatabaseName, @BackupPathLog, @BackupFile'; PRINT @sql; END; IF @Debug IN (0, 1) AND @Execute = 'Y' 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; 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 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for RESTORE DATABASE: @RestoreDatabaseName'; PRINT @sql; END; IF @Debug IN (0, 1) AND @Execute = 'Y' EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'RESTORE DATABASE', @Mode = 1, @DatabaseName = @Database, @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); 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] @Command = @sql, @CommandType = 'ALTER DATABASE', @Mode = 1, @DatabaseName = @Database, @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 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) AND @Execute = 'Y' EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'INTEGRITY CHECK', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; 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 OR @Execute = 'N' BEGIN IF @sql IS NULL PRINT '@sql is NULL for DROP DATABASE: @RestoreDatabaseName'; PRINT @sql; END; IF @Debug IN (0, 1) AND @Execute = 'Y' EXECUTE @sql = [dbo].[CommandExecute] @Command = @sql, @CommandType = 'DROP DATABASE', @Mode = 1, @DatabaseName = @Database, @LogToTable = 'Y', @Execute = 'Y'; END; 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 , @Help BIT = 0, @Version VARCHAR(30) = NULL OUTPUT, @VersionDate DATETIME = NULL OUTPUT, @VersionCheckMode BIT = 0 AS BEGIN SET NOCOUNT ON; SELECT @Version = '3.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; IF @Help = 1 BEGIN PRINT ' /* sp_foreachdb from http://FirstResponderKit.org This script will execute a given command against all, or user-specified, online, readable databases on an instance. 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) 2019 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. Example for basic execution of the stored procedure: exec dbo.sp_foreachdb @command = ''select [name] sys.tables'' ,@database_list = ''Database1,Database2'' ,@exclude_list = ''Database5,OldDatabase''; */ '; RETURN -1; END 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 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_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 -- WITH EXECUTE AS OWNER – maybe not a great idea, depending on the security your system AS BEGIN SET NOCOUNT ON; SELECT @Version = '2.6', @VersionDate = '20190702'; IF(@VersionCheckMode = 1) BEGIN RETURN; END; IF @Help = 1 BEGIN PRINT ' /* sp_ineachdb 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) 2019 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); DECLARE @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)) DECLARE @ServerName AS sysname = CONVERT(sysname, SERVERPROPERTY('ServerName')) -- Stores the SQL Server Instance name. CREATE TABLE #ineachdb(id int, name nvarchar(512)); 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 < 4000), names AS ( SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@database_list, n, CHARINDEX(N',', @database_list + N',', n) - n), 1))) FROM n WHERE n <= LEN(@database_list) AND SUBSTRING(N',' + @database_list, n, 1) = N',' ) INSERT #ineachdb(id,name) SELECT d.database_id, d.name 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) SELECT database_id, name FROM sys.databases; END -- first, let's delete any that have been explicitly excluded IF @exclude_list > N'' -- comma-separated list of potentially valid/invalid/quoted/unquoted names -- exclude trumps include BEGIN ;WITH n(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM n WHERE n < 4000), names AS ( SELECT name = LTRIM(RTRIM(PARSENAME(SUBSTRING(@exclude_list, n, CHARINDEX(N',', @exclude_list + N',', n) - n), 1))) FROM n WHERE n <= LEN(@exclude_list) AND 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 -- 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)) OR (@user_only = 1 AND id IN (1,2,3,4)) OR name NOT LIKE @name_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) ) ); -- 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 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 ); -- 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