----------------------------------------------------------------
-------- Ultimate Compression Savings Estimation Check ---------
----------------------------------------------------------------
-- Author: Eitan Blumin | https://www.eitanblumin.com
-- Create Date: 2019-12-08
-- Source: http://bit.ly/SQLCompressionEstimation
-- Full Link: https://gist.github.com/EitanBlumin/85cf620f7267b234d677f9c3027fb7ce
-- GitHub Repo: https://github.com/MadeiraData/MadeiraToolbox/blob/master/Utility%20Scripts/ultimate_compression_savings_estimation_whole_database.sql
-- Blog: https://eitanblumin.com/2020/02/18/ultimate-compression-savings-estimation-script-entire-database/
----------------------------------------------------------------
-- Description:
-- ------------
-- This script performs compression savings estimation check for both PAGE and ROW
-- compression for an ENTIRE DATABASE.
-- For each index which passes the check, a corresponding ALTER INDEX command
-- is printed for you to use in order to apply the compression.
-- The script also compares the results of the PAGE and ROW estimations and automatically
-- selects the one with the better savings as the command to print, based on the provided thresholds.
-- This script uses mathematical rounding functions FLOOR and CEILING in a manner which makes it more "cautious".
--
-- Some of the algorithms in this script were adapted from the following resources:
-- https://www.sqlservercentral.com/blogs/introducing-what_to_compress-v2
-- https://github.com/microsoft/tigertoolbox/tree/master/Evaluate-Compression-Gains
-- https://github.com/microsoft/azure-sql-tips
----------------------------------------------------------------
-- Change Log:
-- -----------
-- 2021-09-06 - added @ResumableRebuild parameter
-- 2021-09-01 - some minor bug fixes and code quality fixes
-- 2021-02-02 - added SET NOCOUNT, QUOTED_IDENTIFIER, ARITHABORT, XACT_ABORT ON settings
-- 2021-01-17 - added cautionary parameters to stop/pause execution if CPU utilization is too high 
-- 2021-01-17 - added optional cautionary parameters controlling allowed/forbidden execution window
-- 2021-01-17 - made some changes to threshold parameters based on partition stats (adapted from Azure-SQL-Tips)
-- 2021-01-05 - added informative message about current TempDB available space
-- 2020-12-06 - added @MaximumUpdatePctAsInfrequent and @MinimumScanPctForComparison parameters
-- 2020-12-01 - output remediation script is now idempotent; fixed recommendations in resultset to match actual remediation script
-- 2020-09-30 - added @MaxDOP parameter
-- 2020-09-06 - added support for readable secondaries; added MAXDOP 1 for the query from operational stats to avoid access violation bug
-- 2020-03-30 - added filter to ignore indexes and tables with unsupported LOB/FILESTREAM columns
-- 2020-03-16 - added informational and status messages in output script
-- 2020-03-15 - tweaked default parameter values a bit; added server uptime message
-- 2020-02-26 - added best guess for unknown compression recommendations; improved performance for DBs with many objects
-- 2020-02-19 - added support for Azure SQL DB
-- 2020-02-18 - cleaned the code a bit and fixed some bugs
-- 2020-02-17 - added specific database support; adjusted tabs to match github standard; added compatibility checks
-- 2020-02-16 - added threshold parameters; additional checks based on partition stats and operational stats
-- 2019-12-09 - added ONLINE rebuild option
-- 2019-12-24 - flipped to traditional ratio calculation; added READ UNCOMMITTED isolation level; added minimum difference thresholds for PAGE vs. ROW considerations
----------------------------------------------------------------
-- 
-- IMPORTANT !!!
-- -------------
--
-- 1. Don't forget to change the @DatabaseName parameter value to the one you want to check.
--
-- 2. BE MINDFUL IN PRODUCTION ENVIRONMENTS !
--
--		- If you want to be extra careful, run this script with @FeasibilityCheckOnly set to 1. This will perform only basic checks for compression candidates
--		  based on usage and operational stats only, without running [sp_estimate_data_compression_savings].
--
--		- Running this script with @FeasibilityCheckOnly = 0 may take a very long time on big databases with many tables, and significant IO + CPU stress may be noticeable.
--
--		- Schema-level locks may be held for a while per each table, and will possibly block other sessions performing DDL operations.
--
--		- This script uses [sp_estimate_data_compression_savings] which copies 5 % of your data into TempDB and compresses it there.
--		  If you have very large tables, you must be very careful not to fill out the disk.
--		  Please use the following cautionary threshold parameters to avoid such scenarios: @MaxSizeMBForActualCheck, @TempDBSpaceUsageThresholdPercent
----------------------------------------------------------------
-- Parameters:
-- -----------
DECLARE
	-- Choose what to check:
	 @DatabaseName				SYSNAME		= NULL		-- Specify the name of the database to check. If NULL, will use current.
	,@FeasibilityCheckOnly			BIT		= 1		-- If 1, will only check for potential compression candidates, without using sp_estimate_data_compression_savings and without generating remediation scripts
	,@CheckPerPartition			BIT		= 0		-- If 1, will perform separate estimation checks per partition
	,@MinimumSizeMB				INT		= 256		-- Minimum table/partition size in MB in order to perform estimation checks on it

	-- Cautionary thresholds:
	,@MaxSizeMBForActualCheck		INT		= NULL		-- If a table/partition is bigger than this size (in MB), then sp_estimate_data_compression_savings will NOT be executed for it. If set to NULL, then use available TempDB space instead
	,@TempDBSpaceUsageThresholdPercent	INT		= 60		-- A percentage number between 1 and 100, representing how much of the disk space available to TempDB is allowed to be used for the estimation checks

	-- Cautionary parameters to stop/pause execution if CPU utilization is too high (this affects both the estimation check and the rebuild script):
	,@MaximumCPUPercentForRebuild		INT		= 80		-- Maximum average CPU utilization to allow rebuild. Set to NULL to ignore.
	,@SamplesToCheckAvgForCPUPercent	INT		= 10		-- Number of CPU utilization percent samples to check and average out
	,@MaximumTimeToWaitForCPU		DATETIME	= '00:10:00'	-- Maximum time to continously wait for CPU utilization to drop below threshold, before cancelling execution (HH:MM:SS.MS format). Set to NULL to wait forever.

	-- Optional cautionary parameters controlling allowed execution window (server clock)
	-- You can think of this as your "maintenance window" (24hour based. for example, between 0 and 4):
	,@AllowedRuntimeHourFrom		INT		= NULL		-- If not NULL, will specify minimum hour of day during which rebuilds are allowed
	,@AllowedRuntimeHourTo			INT		= NULL		-- If not NULL, will specify maximum hour of day during which rebuilds are allowed

	-- Optional cautionary parameters controlling forbidden execution window (server clock)
	-- You can think of this as your "business hours" (24hour based. for example, between 6 and 22):
	,@NotAllowedRuntimeHourFrom		INT		= 6		-- If not NULL, will specify minimum time of day during which rebuilds are forbidden
	,@NotAllowedRuntimeHourTo		INT		= 22		-- If not NULL, will specify maximum time of day during which rebuilds are forbidden
	 
	-- Threshold parameters controlling recommendation algorithms based on partition stats:
	,@MinimumCompressibleDataPercent	INT		= 45		-- Minimum percent of compressible in-row data, in order to consider any compression
	,@MaximumUpdatePctAsInfrequent		INT		= 10		-- Maximum percent of updates for all operations to consider as "infrequent updates"
	,@MinimumScanPctForComparison		INT		= 5		-- Minimum percent of range scans before considering to compare between update and scan percentages
	,@MinimumScanPctForPage			INT		= 40		-- Minimum percent of scans when comparing to update percent, to deem PAGE compression preferable (otherwise, ROW compression will be preferable)
	,@MaximumUpdatePctForPage		INT		= 40		-- Maximum percent of updates when comparing to scan percent, to deem PAGE compression preferable
	,@MaximumUpdatePctForRow		INT		= 60		-- Maximum percent of updates when comparing to scan percent, to deem ROW compression preferable

	-- Threshold parameters controlling recommendation algorithms based on savings estimation check:
	,@CompressionRatioThreshold		FLOAT		= 45		-- Number between 0 and 100 representing the minimum compressed data ratio, relative to current size, for which a check will pass
	,@CompressionSizeSavedMBThreshold	FLOAT		= 200		-- Minimum estimated saved space in MB resulting from compression (affects both PAGE and ROW compressions)
	,@MinimumRatioDifferenceForPage		FLOAT		= 20		-- Minimum difference in percentage between ROW and PAGE compression types, in order to deem PAGE compression preferable
	,@MinimumSavingsMBDifferenceForPage	FLOAT		= 40		-- Minimum difference in saved space in MB between ROW and PAGE compression types, in order to deem PAGE compression preferable

	-- Parameters controlling the structure of output scripts:
	,@OnlineRebuild				BIT		= 1		-- If 1, will generate REBUILD commands with the ONLINE option turned on
	,@ResumableRebuild			BIT		= 0		-- If 1, will generate REBUILD commands with the RESUMABLE option turned on (SQL 2019 and newer only)
	,@SortInTempDB				BIT		= 1		-- If 1, will generate REBUILD commands with the SORT_IN_TEMPDB option turned on
	,@MaxDOP				INT		= NULL		-- If not NULL, will add a MaxDOP option accordingly. Set to 1 to prevent parallelism and reduce workload.

--------------------------------------------------------------------
--     		  DO NOT CHANGE ANYTHING BELOW THIS LINE       	  --
--------------------------------------------------------------------

SET NOCOUNT, ARITHABORT, XACT_ABORT ON;
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- Variable declaration
DECLARE @CMD NVARCHAR(MAX), @AvailableTempDBSpaceMB INT, @ErrMsg NVARCHAR(MAX)
DECLARE @CurrDB SYSNAME, @Schema SYSNAME, @Table SYSNAME, @ObjectId INT, @IndexId INT, @IndexName SYSNAME, @Partition INT, @CompressionType VARCHAR(4), @EstimationCheckRecommended BIT, @TotalSizeMB INT, @InRowPercent INT, @ScanPercent INT, @UpdatePercent INT
DECLARE @Results AS TABLE ([object_name] SYSNAME NOT NULL, [schema_name] SYSNAME NOT NULL, index_id int NULL, partition_number int NULL, size_w_current_compression_KB float NULL, size_w_requested_compression_KB float NULL, sample_size_current_KB int NULL, sample_size_requested_KB int NULL)
DECLARE @RebuildOptions NVARCHAR(MAX), @ChecksSkipped BIT

-- Compatibility checks
SELECT @ErrMsg = ISNULL(@ErrMsg + ', ', N'Sorry, this SQL Server version is not supported. Missing object(s): ') + objectname
FROM (
SELECT objectname = CONVERT(nvarchar(500), 'sys.tables')
UNION ALL SELECT 'sys.dm_os_volume_stats' WHERE CONVERT(varchar(300),SERVERPROPERTY('Edition')) <> 'SQL Azure'
UNION ALL SELECT 'sys.master_files' WHERE CONVERT(varchar(300),SERVERPROPERTY('Edition')) <> 'SQL Azure'
UNION ALL SELECT 'sys.dm_db_index_operational_stats'
UNION ALL SELECT 'sys.dm_db_partition_stats'
UNION ALL SELECT 'sys.sp_estimate_data_compression_savings'
UNION ALL SELECT 'sys.indexes'
UNION ALL SELECT 'sys.partitions') AS t
WHERE OBJECT_ID(objectname) IS NULL;

IF @ErrMsg IS NOT NULL
BEGIN
	RAISERROR(@ErrMsg,16,1)
	GOTO Quit;
END

-- Init local variables and defaults
SET @RebuildOptions = N''
IF @ResumableRebuild = 1 SET @OnlineRebuild = 1;
IF @OnlineRebuild = 1 SET @RebuildOptions = @RebuildOptions + N', ONLINE = ON'
IF @ResumableRebuild = 1  SET @RebuildOptions = @RebuildOptions + N', RESUMABLE = ON'
IF @SortInTempDB = 1  SET @RebuildOptions = @RebuildOptions + N', SORT_IN_TEMPDB = ON'
IF @MaxDOP IS NOT NULL SET @RebuildOptions = @RebuildOptions + N', MAXDOP = ' + CONVERT(nvarchar(4000), @MaxDOP)
SET @ChecksSkipped = 0;
SET @FeasibilityCheckOnly = ISNULL(@FeasibilityCheckOnly, 1)

-- Validate mandatory percentage parameters
SELECT @ErrMsg = ISNULL(@ErrMsg + CHAR(10), N'Invalid parameter(s): ') + CONVERT(nvarchar(max), N'' + VarName + N' must be a value between 1 and 100')
FROM
(VALUES
	 ('@MinimumCompressibleDataPercent',@MinimumCompressibleDataPercent)
	,('@MinimumScanPctForPage',@MinimumScanPctForPage)
	,('@MaximumUpdatePctForPage',@MaximumUpdatePctForPage)
	,('@MaximumUpdatePctForRow',@MaximumUpdatePctForRow)
	,('@TempDBSpaceUsageThresholdPercent',@TempDBSpaceUsageThresholdPercent)
	,('@CompressionRatioThreshold',@CompressionRatioThreshold)
	,('@MinimumRatioDifferenceForPage',@MinimumRatioDifferenceForPage)
	,('@MaximumUpdatePctAsInfrequent',@MaximumUpdatePctAsInfrequent)
	,('@MinimumScanPctForComparison',@MinimumScanPctForComparison)
	,('@MaximumCPUPercentForRebuild',ISNULL(@MaximumCPUPercentForRebuild,100))
) AS v(VarName,VarValue)
WHERE VarValue NOT BETWEEN 1 AND 100 OR VarValue IS NULL
OPTION (RECOMPILE);

IF @ErrMsg IS NOT NULL
BEGIN
	RAISERROR(@ErrMsg,16,1);
	GOTO Quit;
END

-- Validate hour-based parameters
SELECT @ErrMsg = ISNULL(@ErrMsg + CHAR(10), N'Invalid parameter(s): ') + CONVERT(nvarchar(max), N'' + VarName + N' must be a value between 0 and 23')
FROM
(VALUES
	 ('@AllowedRuntimeHourFrom',@AllowedRuntimeHourFrom)
	,('@AllowedRuntimeHourTo',@AllowedRuntimeHourTo)
	,('@NotAllowedRuntimeHourFrom',@NotAllowedRuntimeHourFrom)
	,('@NotAllowedRuntimeHourTo',@NotAllowedRuntimeHourTo)
) AS v(VarName,VarValue)
WHERE VarValue NOT BETWEEN 0 AND 23 AND VarValue IS NOT NULL
OPTION (RECOMPILE);

IF @ErrMsg IS NOT NULL
BEGIN
	RAISERROR(@ErrMsg,16,1);
	GOTO Quit;
END

IF @FeasibilityCheckOnly = 0 AND CONVERT(varchar(300),SERVERPROPERTY('Edition')) <> 'SQL Azure'
BEGIN
	-- Check free space remaining for TempDB
	SET @CMD = N'USE tempdb;
SELECT @AvailableTempDBSpaceMB = SUM(ISNULL(available_space_mb,0)) FROM
(
-- Get available space inside data files
SELECT 
  vs.volume_mount_point
, available_space_mb = SUM(ISNULL(f.size - FILEPROPERTY(f.[name], ''SpaceUsed''),0)) / 128 
			+ SUM(CASE
				-- If auto growth is disabled
				WHEN f.max_size = 0 THEN 0
				-- If remaining growth size is smaller than remaining disk space, use remaining growth size till max size
				WHEN f.max_size > 0 AND (f.max_size - f.size) / 128 < (vs.available_bytes / 1024 / 1024) THEN (f.max_size - f.size) / 128
				-- Else, do not count available growth for this file
				ELSE 0
			END)
FROM sys.master_files AS f
CROSS APPLY sys.dm_os_volume_stats (f.database_id, f.file_id)  AS vs
WHERE f.database_id = 2
AND f.type = 0
GROUP BY vs.volume_mount_point

UNION ALL

-- Get available space on disk for auto-growth
SELECT 
  vs.volume_mount_point
, available_space_mb = vs.available_bytes / 1024 / 1024
FROM sys.master_files AS f
CROSS APPLY sys.dm_os_volume_stats (f.database_id, f.file_id)  AS vs
WHERE f.database_id = 2
AND f.type = 0
-- If max size is unlimited, or difference between current size and max size is bigger than available disk space
AND (f.max_size = -1 OR (f.max_size > 0 AND (f.max_size - f.size) / 128 > (vs.available_bytes / 1024 / 1024)))
GROUP BY vs.volume_mount_point, vs.available_bytes
) AS q OPTION (RECOMPILE);'

	EXEC sp_executesql @CMD, N'@AvailableTempDBSpaceMB INT OUTPUT', @AvailableTempDBSpaceMB OUTPUT

	-- Use @TempDBSpaceUsageThresholdPercent as a cautionary multiplier
	SET @AvailableTempDBSpaceMB = @AvailableTempDBSpaceMB * 1.0 * (@TempDBSpaceUsageThresholdPercent / 100.0)

	IF @MaxSizeMBForActualCheck > FLOOR(@AvailableTempDBSpaceMB / 0.05)
	BEGIN
		RAISERROR(N'ALERT: %d percent of available TempDB Disk Space is less than 5 percent of specified @MaxSizeMBForActualCheck (%d). Please adjust @MaxSizeMBForActualCheck and/or @TempDBSpaceUsageThresholdPercent accordingly.', 16, 1, @TempDBSpaceUsageThresholdPercent, @MaxSizeMBForActualCheck);
		GOTO Quit;
	END
	ELSE IF ISNULL(@MaxSizeMBForActualCheck,0) <= 0 -- If @MaxSizeMBForActualCheck was not specified, use available TempDB space instead
		SET @MaxSizeMBForActualCheck = FLOOR(@AvailableTempDBSpaceMB / 0.05);
	
	RAISERROR(N'-- TempDB Free Disk Space: %d MB, max size for check: %d MB',0,1,@AvailableTempDBSpaceMB, @MaxSizeMBForActualCheck) WITH NOWAIT;
END

IF @OnlineRebuild = 1 AND ISNULL(CONVERT(int, SERVERPROPERTY('EngineEdition')),0) NOT IN (3,5,8)
BEGIN
	RAISERROR(N'-- WARNING: @OnlineRebuild is set to 1, but current SQL edition does not support ONLINE rebuilds.', 0, 1);
END

IF @ResumableRebuild = 1 AND CONVERT(int, (@@microsoftversion / 0x1000000) & 0xff) < 15
BEGIN
	RAISERROR(N'-- WARNING: @ResumableRebuild is set to 1, but current SQL version does not support RESUMABLE rebuilds.', 0, 1);
END

DECLARE @ObjectsToCheck AS TABLE
(
	[database_name] SYSNAME NOT NULL,
	[schema_name] SYSNAME NOT NULL,
	[table_id] int NULL,
	[table_name] SYSNAME NULL,
	[index_id] INT NULL,
	[index_name] SYSNAME NULL,
	[partition_number] INT NULL,
	size_MB INT NULL,
	in_row_percent INT NULL,
	range_scans_percent INT NULL,
	updates_percent INT NULL
);

-- The following code, making use of @CurrDB variable, is written in a way which would make
-- it easier to rewrite so that it can check multiple databases
/* start of potential for-each-db */
SET @CurrDB = ISNULL(@DatabaseName, DB_NAME())
PRINT N'------------------------------------------------------------------------------------'
PRINT N'------------- Compression Savings Estimation Check by Eitan Blumin -----------------'
PRINT N'---------------- Source: http://bit.ly/SQLCompressionEstimation --------------------'
PRINT N'------------------------------------------------------------------------------------'
PRINT N'--- for Server: ' + @@SERVERNAME + N' , Database: ' + QUOTENAME(@CurrDB)
PRINT N'------------------------------------------------------------------------------------'

DECLARE @SqlStartTime DATETIME, @UpTimeDays INT, @SqlStartTimeString VARCHAR(25);
SELECT @SqlStartTime = sqlserver_start_time FROM sys.dm_os_sys_info;
SET @UpTimeDays = DATEDIFF(dd, @SqlStartTime, GETDATE())
SET @SqlStartTimeString = CONVERT(varchar(25), @SqlStartTime, 121)

RAISERROR(N'--- SQL Server is operational since %s (~%d days)', 0, 1, @SqlStartTimeString, @UpTimeDays) WITH NOWAIT;

-- Check whether TempDB is located on same disk as specified database
IF @SortInTempDB = 1 AND CONVERT(varchar(300),SERVERPROPERTY('Edition')) <> 'SQL Azure'
BEGIN
	IF EXISTS (
		SELECT fs.volume_mount_point
		FROM sys.master_files AS df
		CROSS APPLY sys.dm_os_volume_stats(df.database_id, df.file_id) AS fs
		WHERE df.type = 0
		AND df.database_id = DB_ID(@CurrDB)

		INTERSECT
					
		SELECT fs.volume_mount_point
		FROM sys.master_files AS df
		CROSS APPLY sys.dm_os_volume_stats(df.database_id, df.file_id) AS fs
		WHERE df.type = 0
		AND df.database_id = 2
		)
	RAISERROR(N'-- WARNING: @SortInTempDB is set to 1, but TempDB is located on the same disk drive as specified database "%s".', 0, 1, @CurrDB);
END

-- Make sure specified database is accessible
IF DB_ID(@CurrDB) IS NULL OR DATABASEPROPERTYEX(@CurrDB, 'Updateability') <> 'READ_WRITE' OR DATABASEPROPERTYEX(@CurrDB, 'Status') <> 'ONLINE'
BEGIN
	IF @FeasibilityCheckOnly = 0 OR DB_ID(@CurrDB) IS NULL OR DATABASEPROPERTYEX(@CurrDB, 'Status') <> 'ONLINE'
	BEGIN
		RAISERROR(N'Database "%s" is not valid for compression estimation check. Please make sure it is accessible and writeable.',16,1,@CurrDB);
		GOTO Quit;
	END
	ELSE
		RAISERROR(N'-- NOTE: Database "%s" is not writeable. You will not be able to rebuild its indexes here until it is writeable.',10,1,@CurrDB);
END

-- Get list of all un-compressed tables/partitions in the specified database
-- Use a temp table in order to improve performance for databases with many tables
SET @CMD = N'USE ' + QUOTENAME(@CurrDB) + N';
IF OBJECT_ID(''tempdb..#objects'') IS NOT NULL DROP TABLE #objects;
CREATE TABLE #objects
(
  [schema_name] SYSNAME
, [object_id] INT
, [table_name] SYSNAME
, [index_id] INT NULL
, [index_name] SYSNAME NULL
, [partition_number] INT NULL
, [size_MB] INT
, [in_row_percent] INT
);

INSERT INTO #objects
SELECT
	  OBJECT_SCHEMA_NAME(t.object_id)
	, t.object_id
	, t.name
	, p.index_id
	, ix.name
	, partition_number = ' + CASE WHEN @CheckPerPartition = 1 THEN N'p.partition_number' ELSE N'NULL' END + N'
	, size_MB = CEILING(SUM(ISNULL(sps.in_row_data_page_count,0) + ISNULL(sps.row_overflow_used_page_count,0) + ISNULL(sps.lob_reserved_page_count,0)) / 128.0)
	, in_row_percent = ISNULL(
				FLOOR(SUM(ISNULL(sps.in_row_data_page_count,0)) * 1.0 
				/ NULLIF(SUM(ISNULL(sps.in_row_data_page_count,0) + ISNULL(sps.row_overflow_used_page_count,0) + ISNULL(sps.lob_reserved_page_count,0)),0)
				* 100.0), 0)
FROM sys.tables AS t WITH(NOLOCK)
INNER JOIN sys.partitions AS p WITH(NOLOCK) ON t.object_id = p.object_id AND p.data_compression = 0
INNER JOIN sys.indexes AS ix WITH(NOLOCK) ON ix.object_id = t.object_id AND ix.index_id = p.index_id
LEFT JOIN sys.dm_db_partition_stats AS sps WITH(NOLOCK) ON sps.partition_id = p.partition_id
WHERE 
-- Ignore system objects
    t.is_ms_shipped = 0
AND t.object_id > 255
AND OBJECT_SCHEMA_NAME(t.object_id) <> ''sys''
-- Ignore indexes or tables with unsupported LOB/FILESTREAM columns
AND NOT EXISTS
(
SELECT NULL
FROM sys.columns AS c
INNER JOIN sys.types AS t 
ON c.system_type_id = t.system_type_id
AND c.user_type_id = t.user_type_id
LEFT JOIN sys.index_columns AS ixc
ON ixc.object_id = c.object_id
AND ixc.column_id = c.column_id
AND ix.index_id = ixc.index_id
WHERE (t.[name] in (''text'', ''ntext'', ''image'') OR c.is_filestream = 1)
AND ix.object_id = c.object_id
AND (ix.index_id IN (0,1) OR ixc.index_id IS NOT NULL)
)
GROUP BY
	  t.object_id
	, t.name
	, p.index_id
	, ix.name' 
	+ CASE WHEN @CheckPerPartition = 1 THEN N', p.partition_number' ELSE '' END
+ CASE WHEN ISNULL(@MinimumSizeMB,0) > 0 THEN N'
HAVING
	CEILING(SUM(ISNULL(sps.in_row_data_page_count,0) + ISNULL(sps.row_overflow_used_page_count,0) + ISNULL(sps.lob_reserved_page_count,0)) / 128.0) >= ' + CONVERT(nvarchar(MAX), @MinimumSizeMB) 
ELSE N'' END + N'
OPTION (RECOMPILE);

SELECT
	  DB_NAME()
	, p.[schema_name]
	, p.object_id
	, p.table_name
	, p.index_id
	, p.index_name
	, p.partition_number
	, p.size_MB
	, p.in_row_percent
	, range_scans_percent = ISNULL(
				FLOOR(SUM(ISNULL(ios.range_scan_count,0)) * 1.0 /
				NULLIF(SUM(
					ISNULL(ios.range_scan_count,0) +
					ISNULL(ios.leaf_delete_count,0) + 
					ISNULL(ios.leaf_insert_count,0) + 
					ISNULL(ios.leaf_page_merge_count,0) + 
					ISNULL(ios.leaf_update_count,0) + 
					ISNULL(ios.singleton_lookup_count,0)
				), 0) * 100.0), 0)
	, updates_percent = ISNULL(
				CEILING(SUM(ISNULL(ios.leaf_update_count, 0)) * 1.0 /
				NULLIF(SUM(
					ISNULL(ios.range_scan_count,0) +
					ISNULL(ios.leaf_delete_count,0) + 
					ISNULL(ios.leaf_insert_count,0) + 
					ISNULL(ios.leaf_page_merge_count,0) + 
					ISNULL(ios.leaf_update_count,0) + 
					ISNULL(ios.singleton_lookup_count,0)
				), 0) * 100.0), 0)
FROM #objects AS p WITH(NOLOCK)
OUTER APPLY sys.dm_db_index_operational_stats(db_id(),p.object_id,p.index_id,p.partition_number) AS ios
GROUP BY
	  p.object_id
	, p.schema_name
	, p.table_name
	, p.index_id
	, p.index_name
	, p.partition_number
	, p.size_MB
	, p.in_row_percent
OPTION (RECOMPILE, MAXDOP 1);'

INSERT INTO @ObjectsToCheck
EXEC sp_executesql @CMD;

/* end of potential for-each-db */

-- Init temp table to hold final results
IF OBJECT_ID('tempdb..#ResultsAll') IS NOT NULL DROP TABLE #ResultsAll;
CREATE TABLE #ResultsAll (
	  [database_name] SYSNAME NOT NULL
	, [schema_name] SYSNAME NOT NULL
	, [table_name] SYSNAME NOT NULL
	, [index_name] SYSNAME NULL
	, partition_number INT NULL
	, size_MB INT NULL
	, in_row_percent INT NULL
	, scan_percent INT NULL
	, update_percent INT NULL
	, compression_type VARCHAR(4) NOT NULL
	, compression_ratio FLOAT NULL
	, compression_size_saving_KB FLOAT NULL
	, is_compression_feasible BIT NULL
	, is_compression_recommended bit NULL
)

-- Init cursor to traverse all un-compressed tables that were found
DECLARE TablesToCheck CURSOR
LOCAL FORWARD_ONLY FAST_FORWARD
FOR
SELECT 
	  o.[database_name]
	, o.[schema_name]
	, o.[table_id]
	, o.[table_name]
	, o.[index_id]
	, o.[index_name]
	, o.[partition_number]
	, ct.CompressionType
	, o.size_MB
	, o.in_row_percent
	, o.range_scans_percent
	, o.updates_percent
FROM @ObjectsToCheck AS o
CROSS JOIN (VALUES('PAGE'),('ROW')) AS ct(CompressionType) -- check both ROW and PAGE compression for each

OPEN TablesToCheck

FETCH NEXT FROM TablesToCheck 
INTO @CurrDB, @Schema, @ObjectId, @Table, @IndexId, @IndexName, @Partition, @CompressionType, @TotalSizeMB, @InRowPercent, @ScanPercent, @UpdatePercent

WHILE @@FETCH_STATUS = 0
BEGIN
	DECLARE @sp_estimate_data_compression_savings NVARCHAR(1000);
	SET @sp_estimate_data_compression_savings = QUOTENAME(@CurrDB) + '.sys.sp_estimate_data_compression_savings'

	SET @EstimationCheckRecommended = CASE
						WHEN @InRowPercent < @MinimumCompressibleDataPercent THEN 0
						WHEN ISNULL(@ScanPercent,0) <= @MinimumScanPctForComparison OR ISNULL(@UpdatePercent,0) <= @MaximumUpdatePctAsInfrequent THEN 1
						WHEN @CompressionType = 'PAGE' AND @ScanPercent >= @MinimumScanPctForPage AND @UpdatePercent <= @MaximumUpdatePctForPage THEN 1
						WHEN @CompressionType = 'ROW' AND @UpdatePercent <= @MaximumUpdatePctForRow THEN 1
						ELSE 0
					END

	RAISERROR(N'--Database [%s], table [%s].[%s], index [%s], partition %d: size: %d MB, in-row data: %d percent, range scans: %d percent, updates: %d percent. Compression type: %s',0,1
				,@CurrDB,@Schema,@Table,@IndexName,@Partition,@TotalSizeMB,@InRowPercent,@ScanPercent,@UpdatePercent,@CompressionType) WITH NOWAIT;

	IF @FeasibilityCheckOnly = 0 AND (@MaxSizeMBForActualCheck IS NULL OR (@TotalSizeMB <= @MaxSizeMBForActualCheck AND @TotalSizeMB * 0.05 < @AvailableTempDBSpaceMB))
	BEGIN
		IF @EstimationCheckRecommended = 1
		BEGIN
			BEGIN TRY
			
			IF @MaximumCPUPercentForRebuild IS NOT NULL
			BEGIN
				DECLARE @AvgCPU FLOAT, @WaitForCPUStartTime DATETIME;

				CpuUtilizationCheck:
					SELECT @AvgCPU = AVG( 100 - record.value('(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]', 'int') )
					FROM (
					SELECT TOP (@SamplesToCheckAvgForCPUPercent) [timestamp], convert(xml, record) as record
					FROM sys.dm_os_ring_buffers
					WHERE ring_buffer_type = N'RING_BUFFER_SCHEDULER_MONITOR'
					AND record like '%<SystemHealth>%'
					ORDER BY [timestamp] DESC
					) as RingBufferInfo

					IF @AvgCPU <= @MaximumCPUPercentForRebuild
					BEGIN
						SET @WaitForCPUStartTime = NULL;
						GOTO AfterCpuUtilizationCheck;
					END
					ELSE IF @MaximumTimeToWaitForCPU IS NOT NULL
					BEGIN
						IF @WaitForCPUStartTime IS NULL
						BEGIN
							SET @WaitForCPUStartTime = GETDATE()
						END
						ELSE IF @WaitForCPUStartTime + @MaximumTimeToWaitForCPU > GETDATE()
						BEGIN
							RAISERROR(N'-- CPU utilization is too high. Aborting check.', 16, 1);
							GOTO EndOfCursor;
						END
					END

					WAITFOR DELAY '00:00:00.5'
					GOTO CpuUtilizationCheck;
			END
			AfterCpuUtilizationCheck:

			-- Calculate compression savings estimation
			INSERT INTO @Results
			EXEC @sp_estimate_data_compression_savings
						 @schema_name		= @Schema,  
						 @object_name		= @Table,
						 @index_id		= @IndexId,
						 @partition_number	= @Partition,   
						 @data_compression	= @CompressionType;
			END TRY
			BEGIN CATCH
				DECLARE @EstimationErrMsg NVARCHAR(MAX)
				SET @EstimationErrMsg = ERROR_MESSAGE()
				RAISERROR(N'-- ERROR while running sp_estimate_data_compression_savings: %s',0,1,@EstimationErrMsg);
			END CATCH
		END
		ELSE
		BEGIN
			RAISERROR(N'-- Not a good candidate for compression. Skipping estimation check.',0,1);
			SET @ChecksSkipped = 1;
		END
	END
	ELSE IF @FeasibilityCheckOnly = 1
		SET @ChecksSkipped = 1
	ELSE
	BEGIN
		RAISERROR(N'-- Too big for TempDB. Skipping estimation check.',0,1);
		SET @ChecksSkipped = 1;
	END

	-- Save to main results table
	INSERT INTO #ResultsAll
	SELECT 
		  [database_name]		= @CurrDB
		, [schema_name]			= @Schema
		, table_name			= @Table
		, index_name			= @IndexName
		, partition_number		= @Partition
		, size_MB			= @TotalSizeMB
		, in_row_percent		= @InRowPercent
		, scan_percent			= @ScanPercent
		, update_percent		= @UpdatePercent
		, compression_type		= @CompressionType
		, compression_ratio		= 100 - (SUM(ISNULL(r.size_w_requested_compression_KB,0)) * 100.0 / NULLIF(SUM(ISNULL(r.size_w_current_compression_KB,0)),0))
		, compression_size_saving_KB	= SUM(ISNULL(r.size_w_current_compression_KB,0)) - SUM(ISNULL(r.size_w_requested_compression_KB,0))
		, is_compression_feasible	= @EstimationCheckRecommended
		, is_compression_recommended	= CASE
						WHEN @FeasibilityCheckOnly = 1 OR @EstimationCheckRecommended = 0 THEN NULL
						WHEN
							100 - (SUM(ISNULL(r.size_w_requested_compression_KB,0)) * 100.0 / NULLIF(SUM(ISNULL(r.size_w_current_compression_KB,0)),0)) >= @CompressionRatioThreshold 
						AND (SUM(ISNULL(r.size_w_current_compression_KB,0)) - SUM(ISNULL(r.size_w_requested_compression_KB,0))) / 1024.0 >= @CompressionSizeSavedMBThreshold
						THEN 1
						ELSE 0 END
	FROM
		@Results AS r
	OPTION (RECOMPILE);

	DELETE @Results;

	FETCH NEXT FROM TablesToCheck 
	INTO @CurrDB, @Schema, @ObjectId, @Table, @IndexId, @IndexName, @Partition, @CompressionType, @TotalSizeMB, @InRowPercent, @ScanPercent, @UpdatePercent
END

EndOfCursor:

CLOSE TablesToCheck
DEALLOCATE TablesToCheck

IF @ChecksSkipped = 1
BEGIN
	PRINT N'-- One or more tables were not checked. Please adjust threshold parameter values and make sure you have enough free disk space if you want to run those checks anyway.'
END

-- Return results to client
SELECT
	 [database_name]
	,[schema_name]
	,[table_name]
	,full_table_name		= QUOTENAME([schema_name]) + '.' + QUOTENAME([table_name])
	,index_name
	,partition_number		= ISNULL(CONVERT(varchar(256),partition_number),'ALL')
	,size_MB
	,compressible_data		= CONVERT(varchar(10), in_row_percent) + ' %'
	,scan_percent			= CONVERT(varchar(10), scan_percent) + ' %'
	,update_percent			= CONVERT(varchar(10), update_percent) + ' %'
	,compression_type
	,compression_ratio		= ROUND(compression_ratio,3)
	,compression_size_saving_MB	= compression_size_saving_KB / 1024.0
	,is_compression_candidate	= CASE WHEN is_compression_feasible = 1 THEN 'Yes' ELSE 'No' END
	,is_compression_recommended 	= CASE
						WHEN is_compression_recommended IS NULL AND is_compression_feasible = 1 THEN 
						  CASE
							WHEN in_row_percent < @MinimumCompressibleDataPercent THEN N'No'
							WHEN compression_type = 'PAGE' AND ISNULL(scan_percent,0) <= @MinimumScanPctForComparison AND ISNULL(update_percent,0) <= @MaximumUpdatePctAsInfrequent THEN 'Yes'
							WHEN scan_percent >= @MinimumScanPctForPage AND update_percent <= @MaximumUpdatePctForPage THEN
								CASE WHEN compression_type = 'PAGE' THEN 'Yes' ELSE 'No' END
							WHEN update_percent <= @MaximumUpdatePctForRow THEN
								CASE WHEN compression_type = 'ROW' THEN 'Yes' ELSE 'No' END
							ELSE 'No'
						  END + ' (best guess)'
						WHEN is_compression_recommended = 1 AND SavingsRating = 1 THEN 'Yes' ELSE 'No'
					  END
	,remediation_command		= 
					CASE WHEN ISNULL(is_compression_recommended,0) = 0 OR SavingsRating <> 1 THEN N'-- ' ELSE N'' END
				+ N'USE ' + QUOTENAME([database_name]) + N'; ALTER ' + ISNULL(N'INDEX ' + QUOTENAME(index_name) + N' ON ', N'TABLE ') + QUOTENAME([schema_name]) + '.' + QUOTENAME([table_name]) 
				+ N' REBUILD PARTITION = ' + ISNULL(CONVERT(nvarchar(max),partition_number), N'ALL') 
				+ N' WITH (DATA_COMPRESSION = ' + compression_type + @RebuildOptions + N');'
FROM
(
	SELECT 
	 *, SavingsRating = ROW_NUMBER() OVER (
		PARTITION BY 
			  [database_name]
			, table_name
			, index_name 
			, partition_number
			, is_compression_recommended
		ORDER BY 
			compression_ratio + (CASE WHEN compression_type = 'ROW' THEN @MinimumRatioDifferenceForPage ELSE 0 END) DESC, 
			compression_size_saving_KB + (CASE WHEN compression_type = 'ROW' THEN ISNULL(@MinimumSavingsMBDifferenceForPage,0) * 1024.0 ELSE 0 END) DESC
		)
	FROM #ResultsAll
 ) AS r
ORDER BY
	  [database_name] ASC
	, compression_size_saving_KB DESC
	, compression_ratio DESC
	, size_MB DESC
OPTION (RECOMPILE);

IF @@ROWCOUNT > 0 AND @FeasibilityCheckOnly = 0
BEGIN
	-- Begin generating remediation script that takes into consideration all checks
	-- including ROW vs. PAGE considerations
	DECLARE @PrevDB SYSNAME;
	SET @PrevDB = N'';
	SET @CMD = NULL;
	PRINT N'-----------------------------------------------------------------------'
	PRINT N'---------- Recommendation Script Begins Below -------------------------'
	PRINT N'-----------------------------------------------------------------------'

	DECLARE Rebuilds CURSOR
	LOCAL FORWARD_ONLY FAST_FORWARD
	FOR
	SELECT
	  [database_name]
	, InsertCmd = N'INSERT INTO #INDEXTABLE VALUES (' 
			+ ISNULL(QUOTENAME([index_name], N''''), N'NULL') + N', '
			+ QUOTENAME(QUOTENAME([schema_name]) + N'.' + QUOTENAME([table_name]), N'''') + N', '
			+ ISNULL(CONVERT(nvarchar(max), partition_number), N'NULL') + N', '
			+ QUOTENAME([compression_type], N'''') + N');'
	--, RemediationCmd = N'USE ' + QUOTENAME([database_name]) + N'; ALTER ' + ISNULL(N'INDEX ' + QUOTENAME(index_name) + N' ON ', N'TABLE ') + QUOTENAME([schema_name]) + '.' + QUOTENAME([table_name]) + N'
	--REBUILD PARTITION = ' + ISNULL(CONVERT(nvarchar,partition_number), N'ALL') + N' WITH (DATA_COMPRESSION = ' + compression_type + @RebuildOptions + N');'
	--, StatusMessage = QUOTENAME([database_name]) + N': ' + ISNULL(N'INDEX ' + QUOTENAME(index_name) + N' ON ', N'TABLE ') + QUOTENAME([schema_name]) + N'.' + QUOTENAME([table_name])
	--				+ N' PARTITION = ' + ISNULL(CONVERT(nvarchar,partition_number), N'ALL') + N', DATA_COMPRESSION = ' + compression_type
	FROM
	(
	SELECT
	  [database_name]
	, [schema_name]
	, table_name
	, index_name
	, compression_size_saving_KB
	, compression_ratio
	, compression_type
	, partition_number
	, SavingsRating = ROW_NUMBER() OVER (
				PARTITION BY 
					  [database_name]
					, table_name
					, index_name 
					, partition_number
				ORDER BY 
					compression_ratio + (CASE WHEN compression_type = 'ROW' THEN @MinimumRatioDifferenceForPage ELSE 0 END) DESC, 
					compression_size_saving_KB + (CASE WHEN compression_type = 'ROW' THEN ISNULL(@MinimumSavingsMBDifferenceForPage,0) * 1024.0 ELSE 0 END) DESC
				)
	FROM
		#ResultsAll
	WHERE
		is_compression_recommended = 1
	) AS q
	WHERE SavingsRating = 1
	ORDER BY
	  [database_name] ASC
	, compression_size_saving_KB DESC
	, compression_ratio DESC

	OPEN Rebuilds
	FETCH NEXT FROM Rebuilds INTO @CurrDB, @CMD

	WHILE @@FETCH_STATUS = 0
	BEGIN
		IF @CurrDB <> @PrevDB
		BEGIN
			PRINT N'USE ' + QUOTENAME(@CurrDB) + N';
DECLARE @Size INT, @DB SYSNAME;
SELECT @Size = SUM(FILEPROPERTY([name], ''SpaceUsed'')) / 128.0 FROM sys.database_files WHERE type = 0;
SET @DB = DB_NAME();

RAISERROR(N''Space used for data in "%s" BEFORE compression: %d MB'', 0, 1, @DB, @Size) WITH NOWAIT;
GO
SET NOCOUNT, QUOTED_IDENTIFIER, ARITHABORT, XACT_ABORT ON;
IF OBJECT_ID(''tempdb..#INDEXTABLE'') IS NOT NULL DROP TABLE #INDEXTABLE;
CREATE TABLE #INDEXTABLE (
	IndexName SYSNAME NULL, 
	TableName NVARCHAR(4000) NOT NULL,
	PartitionNumber INT NULL,
	CompressionType SYSNAME NOT NULL
)
'
			SET @PrevDB = @CurrDB;
		END

		PRINT @CMD;

		FETCH NEXT FROM Rebuilds INTO @CurrDB, @CMD;
		
		IF @@FETCH_STATUS <> 0 OR @CurrDB <> @PrevDB
		BEGIN
			PRINT N'GO
USE ' + QUOTENAME(@PrevDB) + N';

DECLARE @WhatIf BIT = 0

SET NOCOUNT, QUOTED_IDENTIFIER, ARITHABORT, XACT_ABORT ON;
DECLARE @DB SYSNAME;
SET @DB = DB_NAME();
DECLARE @time VARCHAR(25)
DECLARE @IndexName SYSNAME, @TableName NVARCHAR(4000), @PartitionNumber INT, @CompressionType SYSNAME, @CMD NVARCHAR(MAX)'

			PRINT N'DECLARE TablesToCheck CURSOR
LOCAL FORWARD_ONLY FAST_FORWARD
FOR
SELECT IndexName, TableName, PartitionNumber, CompressionType,
	Cmd = N''USE '' + QUOTENAME(@DB) + N''; ALTER '' + ISNULL(N''INDEX '' + QUOTENAME(IndexName) + N'' ON '', N''TABLE '') + TableName 
	+ N'' REBUILD PARTITION = '' + ISNULL(CONVERT(nvarchar,PartitionNumber), N''ALL'') 
	+ N'' WITH (DATA_COMPRESSION = '' + CompressionType + N''' + @RebuildOptions + N');''
FROM #INDEXTABLE

OPEN TablesToCheck

WHILE 1 = 1
BEGIN
	FETCH NEXT FROM TablesToCheck INTO @IndexName, @TableName, @PartitionNumber, @CompressionType, @CMD

	IF @@FETCH_STATUS <> 0
		BREAK;'

	IF @AllowedRuntimeHourFrom IS NOT NULL AND @AllowedRuntimeHourTo IS NOT NULL
		PRINT N'
	IF DATEPART(hour, GETDATE()) NOT BETWEEN ' + CONVERT(nvarchar(MAX), @AllowedRuntimeHourFrom) + N' AND ' + CONVERT(nvarchar(MAX), @AllowedRuntimeHourTo) + N'
	BEGIN
		SET @time = CONVERT(VARCHAR(25), GETDATE(), 121);
		RAISERROR(N''%s - Reached outside allowed execution time.'', 0, 1, @time);
		BREAK;
	END'

	IF @NotAllowedRuntimeHourFrom IS NOT NULL AND @NotAllowedRuntimeHourTo IS NOT NULL
		PRINT N'
	IF DATEPART(hour, GETDATE()) BETWEEN ' + CONVERT(nvarchar(MAX), @NotAllowedRuntimeHourFrom) + N' AND ' + CONVERT(nvarchar(MAX), @NotAllowedRuntimeHourTo) + N'
	BEGIN
		SET @time = CONVERT(VARCHAR(25), GETDATE(), 121);
		RAISERROR(N''%s - Reached outside allowed execution time.'', 0, 1, @time);
		BREAK;
	END'

	IF @MaximumCPUPercentForRebuild IS NOT NULL
	PRINT N'
	DECLARE @AvgCPU FLOAT, @WaitForCPUStartTime DATETIME;

	CpuUtilizationCheck:
		SELECT @AvgCPU = AVG( 100 - record.value(''(./Record/SchedulerMonitorEvent/SystemHealth/SystemIdle)[1]'', ''int'') )
		from (
		SELECT TOP (' + CONVERT(nvarchar(MAX), @SamplesToCheckAvgForCPUPercent) + N') [timestamp], convert(xml, record) as record
		FROM sys.dm_os_ring_buffers
		WHERE ring_buffer_type = N''RING_BUFFER_SCHEDULER_MONITOR''
		AND record like ''%<SystemHealth>%''
		ORDER BY [timestamp] DESC
		) as RingBufferInfo

		IF @AvgCPU <= ' + CONVERT(nvarchar(MAX), @MaximumCPUPercentForRebuild) + N'
		BEGIN
			SET @WaitForCPUStartTime = NULL;
		END' + CASE WHEN @MaximumTimeToWaitForCPU IS NOT NULL THEN N'
		ELSE IF @WaitForCPUStartTime IS NULL
		BEGIN
			SET @WaitForCPUStartTime = GETDATE()
			GOTO CpuUtilizationCheck;
		END
		ELSE IF @WaitForCPUStartTime + ' + QUOTENAME(CONVERT(nvarchar(max), @MaximumTimeToWaitForCPU, 121), N'''') + N' > GETDATE()
		BEGIN
			SET @time = CONVERT(VARCHAR(25), GETDATE(), 121);
			RAISERROR(N''%s - CPU utilization is too high.'', 0, 1, @time);
			BREAK;
		END' ELSE N'' END +  N'
		ELSE
		BEGIN
			WAITFOR DELAY ''00:00:00.5''
			GOTO CpuUtilizationCheck;
		END'

	PRINT N'

	-- Check if index has no compression
	IF EXISTS (
	SELECT NULL
	FROM sys.partitions AS p WITH (NOLOCK)
	INNER JOIN sys.indexes AS ix WITH (NOLOCK)
		ON ix.object_id = p.object_id
		AND ix.index_id = p.index_id
	WHERE ix.object_id = OBJECT_ID(@TableName)
	AND (ix.name = @IndexName OR (@IndexName IS NULL AND ix.index_id = 0))
	AND p.data_compression_desc <> @CompressionType
	AND (@PartitionNumber IS NULL OR p.partition_number = @PartitionNumber)
	)
	BEGIN
		SET @time = CONVERT(VARCHAR(25), GETDATE(), 121);
		RAISERROR (N''%s - [%s]: INDEX [%s] ON %s PARTITION = %s, DATA_COMPRESSION = %s'', 0, 1, @time, @DB, @IndexName, @TableName, @PartitionNumber, @CompressionType) WITH NOWAIT;
		PRINT @CMD;
		IF @WhatIf = 0 EXEC sp_executesql @CMD;
	END

END

CLOSE TablesToCheck
DEALLOCATE TablesToCheck

DROP TABLE #INDEXTABLE

GO'
			PRINT N'USE ' + QUOTENAME(@PrevDB) + N';
DECLARE @Size INT, @DB SYSNAME;
SET @DB = DB_NAME();
SELECT @Size = SUM(FILEPROPERTY([name], ''SpaceUsed'')) / 128.0 FROM sys.database_files WHERE type = 0;

RAISERROR(N''Space used for data in "%s" AFTER compression: %d MB'', 0, 1, @DB, @Size) WITH NOWAIT;
GO'
		END --IF @@FETCH_STATUS <> 0 OR @CurrDB <> @PrevDB
	END -- WHILE @@FETCH_STATUS = 0
	PRINT N'DECLARE @time VARCHAR(25) = CONVERT(varchar(25), GETDATE(), 121); RAISERROR(N''%s - Done'',0,1,@time) WITH NOWAIT;'

	CLOSE Rebuilds
	DEALLOCATE Rebuilds

	PRINT N'-----------------------------------------------------------------------'
	PRINT N'----------- Recommendation Script Ends Here  --------------------------'
	PRINT N'-----------------------------------------------------------------------'
END
ELSE IF @FeasibilityCheckOnly = 1
BEGIN
	PRINT N'-----------------------------------------------------------------------'
	PRINT N'-------- Feasibility Check Only - No Script Generated -----------------'
	PRINT N'-----------------------------------------------------------------------'
END
ELSE
BEGIN
	PRINT N'-----------------------------------------------------------------------'
	PRINT N'----------- No Candidates for Compression Found -----------------------'
	PRINT N'-----------------------------------------------------------------------'
END

PRINT N'GO'

Quit: