Blog Post

RYO Maintenance Plan – Update Statistics

,

Another key component of any good maintenance plan is updating statistics. Statistics are what help the SQL Server optimizer choose the best execution plan for your queries. Let your stats get too far out of date, and watch your query performance tank.

For me, the biggest shortcoming of the update statistics maintenance plan task is that it’s pretty much all-or-nothing. It will update stats for all of the objects you specify, whether the data in those objects has changed or not. And this may be fine for smaller databases. But when you start getting into larger amounts of data, updating statistics on everything, even static tables, can be a very time-consuming process. So when I set out to script this particular task, I wanted to include the capability to only update statistics on objects where data has changed since statistics were last updated.

Other than that one enhancement, the script I ended up with is a very straightforward solution, in adherence with the KISS ideology. As with my backup scripts, I wanted to be able to specify a database, or use wildcards to specify multiple databases. Just as the maintenance plan task would, I also wanted to be able to update column statistics, index statistics, or all statistics, and specify a sample size. Unlike the original task, however, the sample size would be a percentage only. Personally, I’ve never had any use for the “rows” sample size option, so I didn’t bother including it here. And lastly, my solution would also include the ability to specify changed objects or all objects. That’s it.

To do all this, the procedure accepts four parameters:

@dbs – This is the database name, i.e. ‘MyDatabase’, ‘My%’. The default is ‘%’ for all databases.
@whatstats – This specifies column statistics only (‘C’), index statistics only (‘I’) or all statistics (‘A’). The default is ‘A’.
@sample – This is the sample size as a percentage, 1 to 100. The default value is 100 for a fullscan.
@changedonly – This is a ‘Y’ or ‘N’ value to specify whether I want to only update statistics on objects where the data has changed since the last time statistics were updated.

So, to update statistics on all objects in all of my databases, with a full scan, I would run

exec maint.sp_update_stats 

If I want to update stats on changed objects in MyDatabase, I would run

exec maint.sp_update_stats @dbs='MyDatabase', @changedonly='Y'

Or maybe I want to update just index statistics on changed objects in MyDatabase using a 50% sample size:

exec maint.sp_update_stats @dbs='MyDatabase', @whatstats='I', @sample=50, @changedonly='Y'

One more thing before I lay the procedure code on you. To determine what objects have changed data, I used the rowmodctr column in sys.sysindexes. Yes, I know that table is deprecated. Yes, I know Microsoft doesn’t recommend using it. But it’s the best option available right now and the table is still there in SQL 2012 RC0. So until it goes away, I’ll continue to use it.

As I mentioned earlier, this is a very simple procedure, therefore I’m not going to step through it like I did with the backup procedures. I’ve documented it pretty thoroughly, so it should be clear to everyone (including myself later on) what I’m doing.

 /* maint.sp_update_stats.sql
Purpose: update statistics for one or more databases
Author: Colleen M. Morrow
Last Edited: 2012-02-17
Instructions: exec maint.sp_update_stats [database|%], [C|I|A], [1-100], [Y|N]
Parameters:
@dbs: database name, accepts wildcards (i.e. 'son%')
@whatstats: what statistics to update, column stats, index stats, or all
@sample: sample size as a percent, 100 for fullscan
@changedonly: update statistics for objects where data has changed since stats last updated
*/SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [maint].[sp_update_stats] (@dbs VARCHAR(128) = '%', @whatstats char(1) = 'A', @sample tinyint = '100', @changedonly char(1) = 'N')
AS
BEGIN
DECLARE
 @tablename varchar(128)
,@schemaname varchar(128)
,@indexname varchar(128)
,@dbname varchar(128)
,@dbid smallint
,@sqlstmt nvarchar(max)
SET NOCOUNT ON
BEGIN TRY
IF @whatstats NOT IN ('C', 'I', 'A')
RAISERROR (
 N'Invalid value for parameter @whatstats.  Please specify C (column stats only), I (index stats only) or A (all  stats).'
,16
,1
);
IF @sample NOT BETWEEN 1 and 100
RAISERROR (
 N'Invalid value for parameter @sample.  Please a value between 1 and 100.'
,16
,1
);
IF @changedonly NOT IN ('Y', 'N')
RAISERROR (
 N'Invalid value for parameter @changedonly.  Please specify Y (only update stats where data has changed) or N (update  stats on all objects).'
,16
,1
);
--loop through all the databases we're covering
DECLARE getdbs CURSOR FOR
SELECT d.database_id,d.name FROM sys.databases d
WHERE d.name <> 'tempdb'
AND d.state = 0
AND d.name LIKE @dbs
OPEN getdbs
FETCH getdbs INTO @dbid, @dbname
WHILE @@FETCH_STATUS = 0
BEGIN
--build the base query; put the objects into a temp table to loop through after
SET @sqlstmt = 'USE '+quotename(@dbname, '[')+'; select s.name as schemaname,t.name as tablename, i.name as indexname '
+'into ##dbtables '
+'from sys.sysindexes i '
+'join sys.tables t on t.object_id = i.id and t.type in (''U'', ''V'', ''TF'') '
+'join sys.schemas s on s.schema_id = t.schema_id '
+'where i.indid > 0 '
--get only changed objects
IF @changedonly = 'y'
SET @sqlstmt = @sqlstmt + 'and i.rowmodctr > 0 '
--exclude indexes
IF @whatstats = 'C'
SET @sqlstmt = @sqlstmt + 'and not exists (select 1 from sys.indexes si where si.object_id = i.id and si.index_id =  i.indid)'
--include only indexes
IF @whatstats = 'I'
SET @sqlstmt = @sqlstmt + 'and exists (select 1 from sys.indexes si where si.object_id = i.id and si.index_id =  i.indid)'
EXEC (@sqlstmt)
--now loop through all our objects and build the update stats statement
DECLARE gettabs CURSOR FOR
SELECT schemaname, tablename, indexname FROM ##dbtables
OPEN gettabs
FETCH gettabs INTO @schemaname, @tablename, @indexname
WHILE @@FETCH_STATUS = 0
BEGIN
SET @sqlstmt = NULL
SET @sqlstmt = 'USE '+quotename(@dbname, '[')+'; UPDATE STATISTICS '+quotename(@schemaname, '[')+'.'+quotename(rtrim (@tablename), '[')+' '+
quotename(@indexname, '[')+' WITH '
IF @sample < 100
SET @sqlstmt = @sqlstmt + 'SAMPLE '+CAST(@sample AS VARCHAR(3))+' PERCENT'
ELSE
SET @sqlstmt = @sqlstmt + 'FULLSCAN'
SET @sqlstmt = @sqlstmt + ';'
EXEC (@sqlstmt)
--PRINT @sqlstmt
FETCH gettabs INTO @schemaname, @tablename, @indexname
END
CLOSE gettabs
DEALLOCATE gettabs
DROP TABLE ##dbtables
FETCH getdbs INTO @dbid, @dbname
END
CLOSE getdbs
DEALLOCATE getdbs
END TRY
BEGIN CATCH
DECLARE @ErrorMessage NVARCHAR(4000);
DECLARE @ErrorSeverity INT;
DECLARE @ErrorState INT;
SELECT @ErrorMessage = ERROR_MESSAGE(),
   @ErrorSeverity = ERROR_SEVERITY(),
   @ErrorState = ERROR_STATE();
-- Use RAISERROR inside the CATCH block to return
-- error information about the original error that
-- caused execution to jump to the CATCH block.
RAISERROR (@ErrorMessage, -- Message text.
   @ErrorSeverity, -- Severity.
   @ErrorState -- State.
   );
END CATCH
END
GO
/* End maint.sp_update_stats.sql */ 

And with that, we’ve covered backups, backup cleanup, and now statistics. We still need to take care of integrity checks, index maintenance and job history cleanup, and those will be covered in future posts.

Rate

You rated this post out of 5. Change rating

Share

Share

Rate

You rated this post out of 5. Change rating