Differenze
Queste sono le differenze tra la revisione selezionata e la versione attuale della pagina.
| Entrambe le parti precedenti la revisione Revisione precedente Prossima revisione | Revisione precedente | ||
| hobby:development:sql:database_scheduler [2026/08/14 11:07] – mauro.cortese | hobby:development:sql:database_scheduler [2026/08/14 14:00] (versione attuale) – mauro.cortese | ||
|---|---|---|---|
| Linea 4: | Linea 4: | ||
| \\ | \\ | ||
| \\ | \\ | ||
| + | |||
| + | Scheduler table-driven in SQL Server, basato su SQL Server Agent con una tabella di configurazione e una stored procedure " | ||
| + | |||
| + | === Architettura === | ||
| + | - **Tabella di configurazione: | ||
| + | - **Tabella di log:** traccia le esecuzioni (successo/ | ||
| + | - **Stored procedure dispatcher: | ||
| + | - **Un solo SQL Server Agent Job:** che gira ogni minuto (o ogni X minuti) e chiama il dispatcher | ||
| + | Vantaggio: non serve creare un job Agent per ogni procedura, ne basta uno solo che orchestra tutto in base alla tabella. | ||
| + | |||
| === Creazione delle tabelle === | === Creazione delle tabelle === | ||
| Linea 12: | Linea 22: | ||
| -- scheduling columns/ | -- scheduling columns/ | ||
| -- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
| - | CREATE TABLE dbo.SchedulerConfig( | + | CREATE TABLE dbo.scheduler_config( |
| IdJob INT IDENTITY(1, | IdJob INT IDENTITY(1, | ||
| JobName | JobName | ||
| Linea 19: | Linea 29: | ||
| Parameters | Parameters | ||
| ScheduleType | ScheduleType | ||
| - | -- DAILY_TIME -> runs at specific times of day, listed in SchedulerConfigTimes | + | -- DAILY_TIME -> runs at specific times of day, listed in scheduler_config_times |
| -- ONE_TIME | -- ONE_TIME | ||
| FrequencyMinutes | FrequencyMinutes | ||
| Linea 29: | Linea 39: | ||
| IsRunning | IsRunning | ||
| LastRunDate | LastRunDate | ||
| - | + | | |
| - | CONSTRAINT | + | CONSTRAINT |
| CHECK (ScheduleType IN (' | CHECK (ScheduleType IN (' | ||
| ); | ); | ||
| GO | GO | ||
| + | |||
| -- ----------------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------------- | ||
| -- 2) FIXED DAILY RUN TIMES | -- 2) FIXED DAILY RUN TIMES | ||
| Linea 40: | Linea 50: | ||
| -- Example: a job can run at both 08:00 and 18:00 by inserting two rows here. | -- Example: a job can run at both 08:00 and 18:00 by inserting two rows here. | ||
| -- ----------------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------------- | ||
| - | CREATE TABLE dbo.SchedulerConfigTimes | + | CREATE TABLE dbo.scheduler_config_times |
| - | | + | |
| - | ConfigID | + | ConfigID |
| RunTime | RunTime | ||
| LastRunDate | LastRunDate | ||
| ); | ); | ||
| GO | GO | ||
| + | |||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
| -- 3) EXECUTION LOG | -- 3) EXECUTION LOG | ||
| Linea 53: | Linea 63: | ||
| -- and any error raised. | -- and any error raised. | ||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
| - | CREATE TABLE dbo.SchedulerLog | + | CREATE TABLE dbo.scheduler_log |
| ID INT IDENTITY(1, | ID INT IDENTITY(1, | ||
| - | ConfigID | + | ConfigID |
| ProcedureName | ProcedureName | ||
| StartDate | StartDate | ||
| Linea 63: | Linea 73: | ||
| ); | ); | ||
| GO | GO | ||
| + | |||
| -- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
| -- 4) SUPPORTING INDEXES | -- 4) SUPPORTING INDEXES | ||
| -- The dispatcher filters on ScheduleType / IsActive / IsRunning on every run | -- The dispatcher filters on ScheduleType / IsActive / IsRunning on every run | ||
| -- (every minute), so these columns need a covering index to avoid a table | -- (every minute), so these columns need a covering index to avoid a table | ||
| - | -- scan as SchedulerConfig | + | -- scan as scheduler_config |
| -- ----------------------------------------------------------------------------- | -- ----------------------------------------------------------------------------- | ||
| - | CREATE NONCLUSTERED INDEX IX_SchedulerConfig_Dispatch | + | CREATE NONCLUSTERED INDEX sX_Schedu_cerConfig_Dispatch |
| - | ON dbo.SchedulerConfig | + | ON dbo.scheduler_config |
| INCLUDE (ProcedureSchema, | INCLUDE (ProcedureSchema, | ||
| GO | GO | ||
| - | + | | |
| - | CREATE NONCLUSTERED INDEX IX_SchedulerConfigTimes_ConfigID | + | CREATE NONCLUSTERED INDEX sX_Schedu_cerConfigTimes_ConfigID |
| - | ON dbo.SchedulerConfigTimes | + | ON dbo.scheduler_config_times |
| INCLUDE (RunTime, LastRunDate); | INCLUDE (RunTime, LastRunDate); | ||
| - | GO</ | + | GO |
| + | |||
| + | </ | ||
| === Dispatcher procedure === | === Dispatcher procedure === | ||
| <sxh sql> | <sxh sql> | ||
| + | |||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
| -- DISPATCHER PROCEDURE | -- DISPATCHER PROCEDURE | ||
| Linea 93: | Linea 106: | ||
| -- are done as set-based statements instead. | -- are done as set-based statements instead. | ||
| -- -------------------------------------------------------------------------- | -- -------------------------------------------------------------------------- | ||
| - | SAMPLE CONFIGURATION ROWS | + | -- SAMPLE CONFIGURATION ROWS |
| -- -------------------------------------------------------------------------- */ | -- -------------------------------------------------------------------------- */ | ||
| - | -- INSERT INTO dbo.SchedulerConfig | + | -- INSERT INTO dbo.scheduler_config |
| -- VALUES (' | -- VALUES (' | ||
| + | |||
| -- DECLARE @NewConfigID INT; | -- DECLARE @NewConfigID INT; | ||
| - | -- INSERT INTO dbo.SchedulerConfig | + | -- INSERT INTO dbo.scheduler_config |
| -- VALUES (' | -- VALUES (' | ||
| -- SET @NewConfigID = SCOPE_IDENTITY(); | -- SET @NewConfigID = SCOPE_IDENTITY(); | ||
| - | -- INSERT INTO dbo.SchedulerConfigTimes | + | -- INSERT INTO dbo.scheduler_config_times |
| -- VALUES (@NewConfigID, | -- VALUES (@NewConfigID, | ||
| - | + | ||
| - | -- INSERT INTO dbo.SchedulerConfig | + | -- INSERT INTO dbo.scheduler_config |
| -- VALUES (' | -- VALUES (' | ||
| -- ---------------------------------------------------------------------------- | -- ---------------------------------------------------------------------------- | ||
| - | CREATE OR ALTER PROCEDURE dbo.sp_SchedulerDispatcher | + | CREATE OR ALTER PROCEDURE dbo.sp_scheduler_dispatcher |
| AS | AS | ||
| BEGIN | BEGIN | ||
| SET NOCOUNT ON; | SET NOCOUNT ON; | ||
| + | |||
| DECLARE @Now DATETIME | DECLARE @Now DATETIME | ||
| DECLARE @Today DATE = CAST(@Now AS DATE); | DECLARE @Today DATE = CAST(@Now AS DATE); | ||
| DECLARE @NowTime TIME = CAST(@Now AS TIME); | DECLARE @NowTime TIME = CAST(@Now AS TIME); | ||
| DECLARE @TodayWeekDay VARCHAR(1) = CAST(DATEPART(WEEKDAY, | DECLARE @TodayWeekDay VARCHAR(1) = CAST(DATEPART(WEEKDAY, | ||
| + | |||
| -- Working table with a row number, used to walk through the due jobs one | -- Working table with a row number, used to walk through the due jobs one | ||
| -- at a time without allocating a CURSOR object. LogID is filled later via | -- at a time without allocating a CURSOR object. LogID is filled later via | ||
| Linea 130: | Linea 143: | ||
| LogID INT NULL | LogID INT NULL | ||
| ); | ); | ||
| + | |||
| -- 1) INTERVAL jobs: due when enough minutes have passed since LastRunDate, | -- 1) INTERVAL jobs: due when enough minutes have passed since LastRunDate, | ||
| -- and (if set) we're inside the allowed time window and weekday list. | -- and (if set) we're inside the allowed time window and weekday list. | ||
| INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| SELECT ID, ProcedureSchema, | SELECT ID, ProcedureSchema, | ||
| - | FROM dbo.SchedulerConfig | + | FROM dbo.scheduler_config |
| WHERE IsActive = 1 | WHERE IsActive = 1 | ||
| AND IsRunning = 0 | AND IsRunning = 0 | ||
| Linea 143: | Linea 156: | ||
| AND (EndTime IS NULL OR @NowTime <= EndTime) | AND (EndTime IS NULL OR @NowTime <= EndTime) | ||
| AND (WeekDays IS NULL OR WeekDays LIKE ' | AND (WeekDays IS NULL OR WeekDays LIKE ' | ||
| + | |||
| -- 2) DAILY_TIME jobs: due when current time has reached a configured | -- 2) DAILY_TIME jobs: due when current time has reached a configured | ||
| -- RunTime slot and that slot hasn't already fired today. | -- RunTime slot and that slot hasn't already fired today. | ||
| INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| SELECT c.ID, c.ProcedureSchema, | SELECT c.ID, c.ProcedureSchema, | ||
| - | FROM dbo.SchedulerConfig | + | FROM dbo.scheduler_config |
| - | INNER JOIN dbo.SchedulerConfigTimes | + | INNER JOIN dbo.scheduler_config_times |
| WHERE c.IsActive = 1 | WHERE c.IsActive = 1 | ||
| AND c.IsRunning = 0 | AND c.IsRunning = 0 | ||
| Linea 156: | Linea 169: | ||
| AND (t.LastRunDate IS NULL OR t.LastRunDate < @Today) | AND (t.LastRunDate IS NULL OR t.LastRunDate < @Today) | ||
| AND CONVERT(CHAR(5), | AND CONVERT(CHAR(5), | ||
| + | |||
| -- 3) ONE_TIME jobs: due once, when current datetime has reached | -- 3) ONE_TIME jobs: due once, when current datetime has reached | ||
| -- SpecificDateTime and the job has never run before. | -- SpecificDateTime and the job has never run before. | ||
| INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | INSERT INTO @DueJobs (ConfigID, ProcedureSchema, | ||
| SELECT ID, ProcedureSchema, | SELECT ID, ProcedureSchema, | ||
| - | FROM dbo.SchedulerConfig | + | FROM dbo.scheduler_config |
| WHERE IsActive = 1 | WHERE IsActive = 1 | ||
| AND IsRunning = 0 | AND IsRunning = 0 | ||
| Linea 167: | Linea 180: | ||
| AND LastRunDate IS NULL | AND LastRunDate IS NULL | ||
| AND @Now >= SpecificDateTime; | AND @Now >= SpecificDateTime; | ||
| + | |||
| -- Nothing due: exit early, no further statements needed. | -- Nothing due: exit early, no further statements needed. | ||
| IF NOT EXISTS (SELECT * FROM @DueJobs) | IF NOT EXISTS (SELECT * FROM @DueJobs) | ||
| RETURN; | RETURN; | ||
| + | |||
| -- Mark every due job as running and stamp LastRunDate in one set-based UPDATE, | -- Mark every due job as running and stamp LastRunDate in one set-based UPDATE, | ||
| -- instead of doing it row-by-row inside the loop. | -- instead of doing it row-by-row inside the loop. | ||
| Linea 177: | Linea 190: | ||
| | | ||
| , | , | ||
| - | FROM dbo.SchedulerConfig | + | FROM dbo.scheduler_config |
| INNER JOIN @DueJobs d ON d.ConfigID = c.ID; | INNER JOIN @DueJobs d ON d.ConfigID = c.ID; | ||
| + | |||
| -- Same for DAILY_TIME slots: stamp all fired slots at once. | -- Same for DAILY_TIME slots: stamp all fired slots at once. | ||
| UPDATE t SET | UPDATE t SET | ||
| t.LastRunDate = @Today | t.LastRunDate = @Today | ||
| - | FROM dbo.SchedulerConfigTimes | + | FROM dbo.scheduler_config_times |
| INNER JOIN @DueJobs d ON d.TimeSlotID = t.ID | INNER JOIN @DueJobs d ON d.TimeSlotID = t.ID | ||
| WHERE d.TimeSlotID IS NOT NULL; | WHERE d.TimeSlotID IS NOT NULL; | ||
| + | |||
| -- Bulk-insert one ' | -- Bulk-insert one ' | ||
| -- IDs directly via OUTPUT so the loop below needs no lookup query. | -- IDs directly via OUTPUT so the loop below needs no lookup query. | ||
| Linea 193: | Linea 206: | ||
| ,LogID INT | ,LogID INT | ||
| ); | ); | ||
| - | + | ||
| - | INSERT INTO dbo.SchedulerLog | + | INSERT INTO dbo.scheduler_log |
| | | ||
| | | ||
| + | |||
| UPDATE d SET | UPDATE d SET | ||
| d.LogID = m.LogID | d.LogID = m.LogID | ||
| FROM @DueJobs d | FROM @DueJobs d | ||
| INNER JOIN @LogMap m ON m.ConfigID = d.ConfigID; | INNER JOIN @LogMap m ON m.ConfigID = d.ConfigID; | ||
| + | |||
| -- Execute each due procedure individually: | -- Execute each due procedure individually: | ||
| -- handling genuinely requires row-by-row processing, so this is a plain | -- handling genuinely requires row-by-row processing, so this is a plain | ||
| Linea 214: | Linea 227: | ||
| DECLARE @LogID INT; | DECLARE @LogID INT; | ||
| DECLARE @SqlCmd NVARCHAR(MAX); | DECLARE @SqlCmd NVARCHAR(MAX); | ||
| + | |||
| WHILE @i <= @Count | WHILE @i <= @Count | ||
| BEGIN | BEGIN | ||
| Linea 225: | Linea 238: | ||
| FROM @DueJobs | FROM @DueJobs | ||
| WHERE RowNum = @i; | WHERE RowNum = @i; | ||
| + | |||
| -- QUOTENAME() protects schema/ | -- QUOTENAME() protects schema/ | ||
| -- reserved-word issues; parameters, if present, are appended as-is. | -- reserved-word issues; parameters, if present, are appended as-is. | ||
| SET @SqlCmd = QUOTENAME(@Schema) + ' | SET @SqlCmd = QUOTENAME(@Schema) + ' | ||
| + | |||
| BEGIN TRY | BEGIN TRY | ||
| EXEC (@SqlCmd); | EXEC (@SqlCmd); | ||
| - | UPDATE dbo.SchedulerLog | + | UPDATE dbo.scheduler_log |
| END TRY | END TRY | ||
| BEGIN CATCH | BEGIN CATCH | ||
| -- One failing procedure never stops the loop: error is logged, | -- One failing procedure never stops the loop: error is logged, | ||
| -- next job proceeds. | -- next job proceeds. | ||
| - | UPDATE dbo.SchedulerLog | + | UPDATE dbo.scheduler_log |
| SET EndDate = GETDATE(), Outcome = ' | SET EndDate = GETDATE(), Outcome = ' | ||
| WHERE ID = @LogID; | WHERE ID = @LogID; | ||
| END CATCH | END CATCH | ||
| + | |||
| -- Always release the running flag, whether the procedure succeeded or failed. | -- Always release the running flag, whether the procedure succeeded or failed. | ||
| - | UPDATE dbo.SchedulerConfig | + | UPDATE dbo.scheduler_config |
| + | |||
| SET @i += 1; | SET @i += 1; | ||
| END | END | ||