From 339307f959e7bae87c9da2f5d6bc56595c09de7c Mon Sep 17 00:00:00 2001 From: kevwhitt-hee Date: Fri, 20 Jun 2025 08:27:00 +0100 Subject: [PATCH] TD-5670 Adds migrations for DB maintenance scripts --- .../202506110906_AddSqlMaintenanceSolution.cs | 17 ++ .../Properties/Resources.Designer.cs | 35 +++ .../Properties/Resources.resx | 6 + .../TD-5670-MaintenanceScripts_DOWN.sql | 4 + .../Scripts/TD-5670-MaintenanceScripts_UP.sql | 204 ++++++++++++++++++ 5 files changed, 266 insertions(+) create mode 100644 DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs create mode 100644 DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql create mode 100644 DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql diff --git a/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs new file mode 100644 index 0000000000..86b2026fc4 --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/202506110906_AddSqlMaintenanceSolution.cs @@ -0,0 +1,17 @@ +namespace DigitalLearningSolutions.Data.Migrations +{ + using FluentMigrator; + [Migration(202506110906)] + public class AddSqlMaintenanceSolution : Migration + { + + public override void Up() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_UP); + } + public override void Down() + { + Execute.Sql(Properties.Resources.TD_5670_MaintenanceScripts_DOWN); + } + } +} diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs index f5949119d4..513aa404b9 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.Designer.cs @@ -2524,6 +2524,41 @@ internal static string TD_5514_Alter_SendExpiredTBCReminders_Up { } } + /// + /// Looks up a localized string similar to IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; + ///IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///. + /// + internal static string TD_5670_MaintenanceScripts_DOWN { + get { + return ResourceManager.GetString("TD-5670-MaintenanceScripts_DOWN", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to -- ============================================ + ///-- CommandLog table + ///-- ============================================ + ///IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; + ///CREATE TABLE dbo.CommandLog ( + /// ID INT IDENTITY PRIMARY KEY, + /// DatabaseName SYSNAME NULL, + /// SchemaName SYSNAME NULL, + /// ObjectName SYSNAME NULL, + /// ObjectType CHAR(2) NULL, + /// IndexName SYSNAME NULL, + /// IndexType TINYINT NULL, + /// StatisticsName SYSNAME NULL, + /// PartitionNumber INT NULL, + /// Ext [rest of string was truncated]";. + /// + internal static string TD_5670_MaintenanceScripts_UP { + get { + return ResourceManager.GetString("TD_5670_MaintenanceScripts_UP", resourceCulture); + } + } + /// /// Looks up a localized string similar to /****** Object: StoredProcedure [dbo].[GetActiveAvailableCustomisationsForCentreFiltered_V6] Script Date: 29/09/2022 19:11:04 ******/ ///SET ANSI_NULLS ON diff --git a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx index c0b77842ad..e800a84b96 100644 --- a/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx +++ b/DigitalLearningSolutions.Data.Migrations/Properties/Resources.resx @@ -487,4 +487,10 @@ ..\Scripts\TD-5447-Alter_ReorderFrameworkCompetency_Up.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + + ..\Scripts\TD-5670-MaintenanceScripts_UP.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + + + ..\Scripts\TD-5670-MaintenanceScripts_DOWN.sql;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-8 + \ No newline at end of file diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql new file mode 100644 index 0000000000..14a45533ad --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_DOWN.sql @@ -0,0 +1,4 @@ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.sp_purge_commandlog', 'P') IS NOT NULL DROP PROCEDURE dbo.sp_purge_commandlog; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; diff --git a/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql new file mode 100644 index 0000000000..ac8258b6fa --- /dev/null +++ b/DigitalLearningSolutions.Data.Migrations/Scripts/TD-5670-MaintenanceScripts_UP.sql @@ -0,0 +1,204 @@ +-- ============================================ +-- Drop if exists (for clean redeploy) +-- ============================================ +IF OBJECT_ID('dbo.IndexOptimize', 'P') IS NOT NULL DROP PROCEDURE dbo.IndexOptimize; +IF OBJECT_ID('dbo.DatabaseIntegrityCheck', 'P') IS NOT NULL DROP PROCEDURE dbo.DatabaseIntegrityCheck; +IF OBJECT_ID('dbo.CommandExecute', 'P') IS NOT NULL DROP PROCEDURE dbo.CommandExecute; +IF OBJECT_ID('dbo.CommandLog', 'U') IS NOT NULL DROP TABLE dbo.CommandLog; +GO + +-- ============================================ +-- CommandLog table +-- ============================================ +CREATE TABLE dbo.CommandLog ( + ID INT IDENTITY PRIMARY KEY, + DatabaseName SYSNAME NULL, + SchemaName SYSNAME NULL, + ObjectName SYSNAME NULL, + ObjectType CHAR(2) NULL, + IndexName SYSNAME NULL, + IndexType TINYINT NULL, + StatisticsName SYSNAME NULL, + PartitionNumber INT NULL, + ExtendedInfo XML NULL, + Command NVARCHAR(MAX) NOT NULL, + CommandType NVARCHAR(60) NOT NULL, + StartTime DATETIME NOT NULL, + EndTime DATETIME NOT NULL, + ErrorNumber INT NOT NULL, + ErrorMessage NVARCHAR(MAX) NULL +); +GO + +-- ============================================ +-- CommandExecute stored procedure +-- ============================================ +CREATE PROCEDURE dbo.CommandExecute + @Command NVARCHAR(MAX), + @CommandType NVARCHAR(60), + @DatabaseName SYSNAME = NULL, + @SchemaName SYSNAME = NULL, + @ObjectName SYSNAME = NULL, + @ObjectType CHAR(2) = NULL, + @IndexName SYSNAME = NULL, + @IndexType TINYINT = NULL, + @StatisticsName SYSNAME = NULL, + @PartitionNumber INT = NULL, + @ExtendedInfo XML = NULL +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @StartTime DATETIME = GETDATE(); + DECLARE @ErrorNumber INT = 0; + DECLARE @ErrorMessage NVARCHAR(MAX) = NULL; + + BEGIN TRY + EXEC (@Command); + END TRY + BEGIN CATCH + SET @ErrorNumber = ERROR_NUMBER(); + SET @ErrorMessage = ERROR_MESSAGE(); + END CATCH; + + INSERT INTO dbo.CommandLog ( + DatabaseName, SchemaName, ObjectName, ObjectType, IndexName, IndexType, StatisticsName, + PartitionNumber, ExtendedInfo, Command, CommandType, StartTime, EndTime, ErrorNumber, ErrorMessage + ) + VALUES ( + @DatabaseName, @SchemaName, @ObjectName, @ObjectType, @IndexName, @IndexType, @StatisticsName, + @PartitionNumber, @ExtendedInfo, @Command, @CommandType, @StartTime, GETDATE(), @ErrorNumber, @ErrorMessage + ); + + IF @ErrorNumber <> 0 + RAISERROR(@ErrorMessage, 16, 1); +END +GO + +-- ============================================ +-- IndexOptimize stored procedure +-- ============================================ +CREATE PROCEDURE dbo.IndexOptimize + @Databases NVARCHAR(MAX) = 'USER_DATABASES', + @FragmentationMedium TINYINT = 30, + @FragmentationHigh TINYINT = 70 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @sql NVARCHAR(MAX) = N' + USE [' + @db + ']; + + DECLARE @schema SYSNAME, @table SYSNAME, @index SYSNAME; + DECLARE @index_id INT, @frag FLOAT; + + DECLARE c CURSOR FOR + SELECT s.name, t.name, i.name, i.index_id, ips.avg_fragmentation_in_percent + FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, ''LIMITED'') ips + JOIN sys.indexes i ON i.object_id = ips.object_id AND i.index_id = ips.index_id + JOIN sys.tables t ON t.object_id = ips.object_id + JOIN sys.schemas s ON s.schema_id = t.schema_id + WHERE ips.index_id > 0 AND ips.page_count > 100; + + OPEN c; + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = ''ALTER INDEX ['' + @index + ''] ON ['' + @schema + ''].['' + @table + ''] ''; + + IF @frag >= ' + CAST(@FragmentationHigh AS NVARCHAR) + ' + SET @cmd += ''REBUILD''; + ELSE IF @frag >= ' + CAST(@FragmentationMedium AS NVARCHAR) + ' + SET @cmd += ''REORGANIZE''; + ELSE + SET @cmd = NULL; + + IF @cmd IS NOT NULL + EXEC dbo.CommandExecute @Command = @cmd, + @CommandType = ''ALTER INDEX'', + @DatabaseName = ''' + @db + ''', + @SchemaName = @schema, + @ObjectName = @table, + @ObjectType = ''U'', + @IndexName = @index; + + FETCH NEXT FROM c INTO @schema, @table, @index, @index_id, @frag; + END; + + CLOSE c; + DEALLOCATE c; + '; + + EXEC sp_executesql @sql; + FETCH NEXT FROM db_cursor INTO @db; + END; + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- DatabaseIntegrityCheck stored procedure +-- ============================================ +CREATE PROCEDURE dbo.DatabaseIntegrityCheck + @Databases NVARCHAR(MAX) = 'USER_DATABASES' +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @db SYSNAME; + DECLARE db_cursor CURSOR FOR + SELECT name FROM sys.databases + WHERE (@Databases = 'USER_DATABASES' AND database_id > 4) + OR name = @Databases; + + OPEN db_cursor; + FETCH NEXT FROM db_cursor INTO @db; + + WHILE @@FETCH_STATUS = 0 + BEGIN + DECLARE @cmd NVARCHAR(MAX); + SET @cmd = 'DBCC CHECKDB([' + @db + ']) WITH NO_INFOMSGS, ALL_ERRORMSGS'; + + EXEC dbo.CommandExecute + @Command = @cmd, + @CommandType = 'DBCC CHECKDB', + @DatabaseName = @db; + + FETCH NEXT FROM db_cursor INTO @db; + END + + CLOSE db_cursor; + DEALLOCATE db_cursor; +END +GO + +-- ============================================ +-- Purge command log stored procedure +-- ============================================ +CREATE OR ALTER PROCEDURE dbo.sp_purge_commandlog + @DaysToKeep INT = 30 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeleteBefore DATETIME = DATEADD(DAY, -@DaysToKeep, GETDATE()); + + DELETE FROM dbo.CommandLog + WHERE StartTime < @DeleteBefore; +END +GO