Difference between revisions of "Database Maintenance Script"

From TMS Support Wiki
Jump to navigation Jump to search
Line 80: Line 80:
Print('--Index Management--')
Print('--Index Management--')


DECLARE @MinimumImpactThreshold int = 1000
DECLARE @MinimumNewIndexImpact int = 1000
DECLARE @fillfactor int = 80
DECLARE @MaximumBadIndexMagnitude int = 5
DECLARE @BadIndexImpactThreshold int = 5000
DECLARE @FillFactor int = 80


Print('Dropping bad indexes.')
Print('Dropping bad indexes.')
Line 89: Line 89:
DECLARE index_cursor CURSOR
DECLARE index_cursor CURSOR
FOR
FOR
SELECT 'DROP INDEX IF EXISTS [' + IX.name + '] ON [' + OBJECT_NAME(IX.OBJECT_ID) + '] -- IMPACT: ' + CAST((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1)) * 1000 AS varchar)
SELECT 'DROP INDEX IF EXISTS [' + IX.name + '] ON [' + OBJECT_NAME(IX.OBJECT_ID) + '] -- IMPACT: ' + CAST((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1)) AS varchar)
FROM sys.indexes IX
FROM sys.indexes IX
INNER JOIN sys.dm_db_index_usage_stats IXUS ON IXUS.index_id = IX.index_id AND IXUS.OBJECT_ID = IX.OBJECT_ID
INNER JOIN sys.dm_db_index_usage_stats IXUS ON IXUS.index_id = IX.index_id AND IXUS.OBJECT_ID = IX.OBJECT_ID
INNER JOIN sys.dm_db_partition_stats PS on PS.object_id=IX.object_id
INNER JOIN sys.dm_db_partition_stats PS on PS.object_id=IX.object_id
WHERE OBJECTPROPERTY(IX.OBJECT_ID,'IsUserTable') = 1 and IX.name like '%IX%'
WHERE OBJECTPROPERTY(IX.OBJECT_ID,'IsUserTable') = 1 and IX.name like '%IX%'
AND ((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1)) * 1000) > @BadIndexImpactThreshold
AND ((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1))) > @MaximumBadIndexMagnitude
GROUP BY OBJECT_NAME(IX.OBJECT_ID) ,IX.name ,IX.type_desc ,IXUS.user_seeks ,IXUS.user_scans ,IXUS.user_lookups,IXUS.user_updates ,IXUS.last_user_seek ,IXUS.last_user_scan ,IXUS.last_user_lookup ,IXUS.last_user_update
GROUP BY OBJECT_NAME(IX.OBJECT_ID) ,IX.name ,IX.type_desc ,IXUS.user_seeks ,IXUS.user_scans ,IXUS.user_lookups,IXUS.user_updates ,IXUS.last_user_seek ,IXUS.last_user_scan ,IXUS.last_user_lookup ,IXUS.last_user_update
ORDER BY IXUS.user_updates DESC
ORDER BY IXUS.user_updates DESC
Line 146: Line 146:
INNER JOIN sys.dm_db_missing_index_details dm_mid ON dm_mig.index_handle = dm_mid.index_handle
INNER JOIN sys.dm_db_missing_index_details dm_mid ON dm_mig.index_handle = dm_mid.index_handle
WHERE dm_mid.database_ID = DB_ID()
WHERE dm_mid.database_ID = DB_ID()
AND dm_migs.avg_user_impact * (dm_migs.user_seeks + dm_migs.user_scans) > @MinimumImpactThreshold
AND dm_migs.avg_user_impact * (dm_migs.user_seeks + dm_migs.user_scans) > @MinimumNewIndexImpact


OPEN db_cursor
OPEN db_cursor
Line 186: Line 186:
   WHILE @@FETCH_STATUS = 0   
   WHILE @@FETCH_STATUS = 0   
   BEGIN   
   BEGIN   
       SET @cmd = 'ALTER INDEX ALL ON ' + @Table + ' REBUILD WITH (FILLFACTOR = ' + CONVERT(VARCHAR(3),@fillfactor) + ')'  
       SET @cmd = 'ALTER INDEX ALL ON ' + @Table + ' REBUILD WITH (FILLFACTOR = ' + CONVERT(VARCHAR(3),@FillFactor) + ')'  
       Print(@cmd)
       Print(@cmd)
       EXEC (@cmd)   
       EXEC (@cmd)   

Revision as of 12:50, 13 March 2024

In common with most database driven applications there is some routine maintenance that should be performed periodically on the PTS 5 database to keep it working optimally. As such we have developed a script that can be executed ad-hoc or implemented into an SQL maintenance plan, that will perform the following tasks:

  1. Trim the ELMAH error table
    By default the ELMAH error logging system logs and stores every application and server error indefinitely, which can become a waste of disk space.
  2. Fix broken scanning sequence references
    In older versions of PTS 5, in limited circumstances, it was possible to delete an activity that was part of the scanning sequence for a prescription type, which would cause an error when the prescription type was subsequently booked in.
  3. Adjust erroneous prescription start times
    Runs the script to retroactively fix the issue discussed here. Has no effect on later versions of PTS where this issue was permanently addressed.
  4. Index the database based on real world usage
  5. Drop indexes that are deemed to be slowing down updates more than they are speeding up reads
  6. Rebuild indexes
  7. Reports the amount of prescriptions that are waiting for collection or delivery
    If this number is suspiciously large, it can represent a configuration issue (described here) that will eventually lead to performance degradation.

It's not uncommon for a TMSi support technician to recommend that the script be executed to clear up various issues. If so, please be sure to send them the output from the Messages tab.

Prerequisites

The executing user account (or the SQL Agent account in the case of a maintenance plan) must have the view server state permission for the script to work correctly. This permission allows the script to retrieve database metrics and therefore allows it to suggest and apply suitable indexes.

You can grant the view server state permission in Security > Logins > [right-click the user account] > Properties > Securables > View server state > Grant.

Furthermore, TMSi are very unlikely to have permission to create or modify a maintenance plan on your SQL Server, so this task should be delegated to a DBA.

Stored procedure details

Starting with PTS v5.0.3.9, the maintenance script is pre-installed as a stored procedure named MaintenanceScript. A DBA will be able to execute it for you or add it to a maintenance plan.

If you have an earlier version of PTS 5, you can copy the script below.

Parameters

All parameters are optional and the default values are fine unless you have an edge case.

  1. ErrorDaysToKeep
    This specifies the number of days to retain in the ELMAH error log. This defaults to 0, meaning no errors are kept and the table is truncated instead. Anything other than zero will significantly increase the amount of time it takes for the script to complete. You should also not change this setting if you have full transaction logging enabled.
  2. MinimumNewIndexImpact
    Based on SQL Server's missing indexes report. The missing index impact is calculated as the sum of user seeks and user reads, multiplied by the average user impact. If this value is greater than the MinimumNewIndexImpact parameter, the index is applied.
  3. MaximumBadIndexMagnitude
    Based on SQL Server's index usage stats. The bad index impact is calculated as the total user updates, divided by the sum of all user seeks, scans and lookups. If this value is greater than the MaximumBadIndexMagnitude parameter, the index is dropped.
  4. FillFactor
    The fill factor of the databases indexes. Default is 80 percent.

Script

The script is subject to change without notice and more tasks may be added in the future.

DECLARE @ErrorDaysToKeep int = 0

DECLARE @WFC int
SELECT @WFC = (SELECT COUNT(Prescriptions.PrescriptionID) FROM [Prescriptions] WHERE [Status] = 4)
print ('There are ' + CAST(@WFC AS VARCHAR) + ' prescriptions waiting for collection.')

DECLARE @WFD int
SELECT @WFD = (SELECT COUNT(Prescriptions.PrescriptionID) FROM [Prescriptions] WHERE [Status] = 6)
print ('There are ' + CAST(@WFD AS VARCHAR) + ' prescriptions waiting for delivery.')

IF @ErrorDaysToKeep = 0
BEGIN
Print('Truncating Elmah table.')
TRUNCATE TABLE ELMAH_Error
Print('Truncated.')
END
ELSE
BEGIN
Print('Deleting from Elmah table.')
DELETE FROM ELMAH_Error WHERE TimeUtc < dateadd(day, -@ErrorDaysToKeep, getdate())
Print('Deleted from Elmah table.')
END

Print('Fixing broken scanning sequence references.')
DELETE FROM PrescriptionTypeScanningSequences WHERE PrescriptionTypeScanningSequences.ActivityId IN (SELECT PrescriptionTypeScanningSequences.ActivityId FROM PrescriptionTypeScanningSequences LEFT OUTER JOIN Activities ON PrescriptionTypeScanningSequences.ActivityId = Activities.ActivityID WHERE Activities.ActivityID IS NULL)

Print('Fixing erroneous start times.')
UPDATE Prescriptions SET [Started] = [Created], [Requested] = [Created], [TargetCompletionTime] = DATEADD(ss, DATEDIFF(ss, [Started], [TargetCompletionTime]), [Created]), [TargetDeliveryTime] = DATEADD(ss, DATEDIFF(ss, [Started], [TargetDeliveryTime]), [Created]) WHERE ([created] > [Started] OR [created] > Requested) AND DATEDIFF(ss, [Started], [Created]) > 60 AND [Requested] = [Started]

Print('Clearing devices table.')
TRUNCATE TABLE [Devices]

--Index management--
Print('--Index Management--')

DECLARE @MinimumNewIndexImpact int = 1000
DECLARE @MaximumBadIndexMagnitude int = 5
DECLARE @FillFactor int = 80

Print('Dropping bad indexes.')
DECLARE @DropStatement VARCHAR(max)

DECLARE index_cursor CURSOR
FOR
SELECT 'DROP INDEX IF EXISTS [' + IX.name + '] ON [' + OBJECT_NAME(IX.OBJECT_ID) + '] -- IMPACT: ' + CAST((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1)) AS varchar)
FROM sys.indexes IX
INNER JOIN sys.dm_db_index_usage_stats IXUS ON IXUS.index_id = IX.index_id AND IXUS.OBJECT_ID = IX.OBJECT_ID
INNER JOIN sys.dm_db_partition_stats PS on PS.object_id=IX.object_id
WHERE OBJECTPROPERTY(IX.OBJECT_ID,'IsUserTable') = 1 and IX.name like '%IX%'
AND ((IXUS.user_updates / (IXUS.user_seeks + IXUS.user_scans + IXUS.user_lookups + 1))) > @MaximumBadIndexMagnitude
GROUP BY OBJECT_NAME(IX.OBJECT_ID) ,IX.name ,IX.type_desc ,IXUS.user_seeks ,IXUS.user_scans ,IXUS.user_lookups,IXUS.user_updates ,IXUS.last_user_seek ,IXUS.last_user_scan ,IXUS.last_user_lookup ,IXUS.last_user_update
ORDER BY IXUS.user_updates DESC

OPEN index_cursor

FETCH NEXT
FROM index_cursor
INTO @DropStatement

WHILE @@FETCH_STATUS = 0
BEGIN
	PRINT (@DropStatement)

	EXEC (@DropStatement)

	PRINT ('Index dropped.')

	FETCH NEXT
	FROM index_cursor
	INTO @DropStatement
END

CLOSE index_cursor

DEALLOCATE index_cursor

IF @DropStatement IS NULL
BEGIN
Print('No bad indexes within impact threshold.')
END

Print('Generating create index statements.')
DECLARE @CreateStatement VARCHAR(max)

DECLARE db_cursor CURSOR
FOR
SELECT 'CREATE INDEX [IX_' + OBJECT_NAME(dm_mid.OBJECT_ID, dm_mid.database_id) + '_' + REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.equality_columns, ''), ', ', '_'), '[', ''), ']', '') + CASE 
		WHEN dm_mid.equality_columns IS NOT NULL
			AND dm_mid.inequality_columns IS NOT NULL
			THEN '_'
		ELSE ''
		END + REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.inequality_columns, ''), ', ', '_'), '[', ''), ']', '') + '_' + CONVERT(VARCHAR(255), NEWID()) + ']' + ' ON ' + dm_mid.statement + ' (' + ISNULL(dm_mid.equality_columns, '') + CASE 
		WHEN dm_mid.equality_columns IS NOT NULL
			AND dm_mid.inequality_columns IS NOT NULL
			THEN ','
		ELSE ''
		END + ISNULL(dm_mid.inequality_columns, '') + ')' + ISNULL(' INCLUDE (' + dm_mid.included_columns + ')', '') + ' --IMPACT: ' + CAST(dm_migs.avg_user_impact * (dm_migs.user_seeks + dm_migs.user_scans) AS varchar)
FROM sys.dm_db_missing_index_groups dm_mig
INNER JOIN sys.dm_db_missing_index_group_stats dm_migs ON dm_migs.group_handle = dm_mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details dm_mid ON dm_mig.index_handle = dm_mid.index_handle
WHERE dm_mid.database_ID = DB_ID()
	AND dm_migs.avg_user_impact * (dm_migs.user_seeks + dm_migs.user_scans) > @MinimumNewIndexImpact

OPEN db_cursor

FETCH NEXT
FROM db_cursor
INTO @CreateStatement

WHILE @@FETCH_STATUS = 0
BEGIN
	PRINT (@CreateStatement)

	EXEC (@CreateStatement)

	PRINT ('Index created successfully.')

	FETCH NEXT
	FROM db_cursor
	INTO @CreateStatement
END

CLOSE db_cursor

DEALLOCATE db_cursor

IF @CreateStatement IS NULL
BEGIN
Print('No missing indexes within impact threshold.')
END

Print('Rebuilding existing indexes')

DECLARE @Table VARCHAR(255)  
DECLARE @cmd NVARCHAR(500)  

DECLARE TableCursor CURSOR FOR SELECT TABLE_SCHEMA + '.' + TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' 
OPEN TableCursor   
   FETCH NEXT FROM TableCursor INTO @Table   
   WHILE @@FETCH_STATUS = 0   
   BEGIN   
       SET @cmd = 'ALTER INDEX ALL ON ' + @Table + ' REBUILD WITH (FILLFACTOR = ' + CONVERT(VARCHAR(3),@FillFactor) + ')' 
       Print(@cmd)
       EXEC (@cmd)   
       FETCH NEXT FROM TableCursor INTO @Table   
   END   
CLOSE TableCursor   
DEALLOCATE TableCursor
PRINT ('Indexes rebuilt.')

SELECT 'The maintenance script has finished executing. If a support technician has asked you to run this, please send them a copy of the output from the Messages tab.'

Running the stored procedure

A DBA will be able to execute the script for you using the SQL Server Management Studio UI, or by using this script if the stored procedure is installed:

USE [pharmacyDB] GO EXEC [dbo].[MaintenanceScript] GO

Be sure to enter the correct database name, if it's not the default "pharmacyDB".

Running it in a maintenance plan

The maintenance plan should be scheduled to run out of hours as there is a marginal performance impact while it is executing. If you already have a PTS maintenance plan in place, such as for backing up, it's fine to add this script to the same plan - but have the backup task occur first.

Ideally the script should be set to run once per week but this is flexible. Running it too infrequently can expose you to the problems the script aims to mitigate.

To configure the maintenance plan to execute the maintenance script, add an "Execute T-SQL Statement" task to it from the toolbox, and set it up to either execute the entire script by copy & pasting it from above, or preferably, have it execute the stored procedure if it's available.