Files
sql-scripts/DBG - compression saving estimate.sql
2023-10-23 11:12:58 +02:00

911 lines
40 KiB
Transact-SQL

----------------------------------------------------------------
-------- 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: