Automating Sliding Window Maintenance
I mentioned in my last post that partitioning provides a great way to manage a time-based sliding window. Unfortunately, there are no out-of-the-box tools to facilitate adjusting the sliding window so I decided to share scripts I’ve developed to automate the task.
Details of a time-based sliding window strategy vary slightly depending on whether the partition function is RANGE RIGHT or RANGE LEFT. The default RANGE LEFT is pervasive so I’ll post a RANGE LEFT example here. I’ll follow up with a RANGE RIGHT version in my next post.
The stored procedure below shows how you can automate a RANGE LEFT daily sliding window for a datetime data type partition function. This sample procedure can be adjusted for other time-including data types simply by changing the data type declarations and adjusting the boundary calculation to return the latest possible time. See my Sliding Window Table Partitioning post for a list of the appropriate time value boundary values by data type.
The @RetentionDays parameter specifies the desired data retention in days. This value is used to purge old data using SWITCH and also adjust the partition function so that historical, current and future data are in separate partitions. See my Sliding Window Table Partitioning post for more information on how this approach avoids costly data movement during SPLIT and MERGE.
This sample stored proc can be tweaked in order to address environment-specific requirements (object names, filegroups, multiple partition schemes) other time intervals (e.g. month, year). I’ve found the verbose messages (SELECT and RAISERROR) are useful for troubleshooting but these can be removed if desired.
Note that this sample uses a staging table as a SWITCH target that is partitioned identically to the primary table. I’ve found that this technique makes it easy to ensure the staging table target partition is properly aligned for SWITCH.
Example of RANGE LEFT Sliding Window Automation
Below are the demo objects used by the range LEFT sliding window procedure.
--no boundaries initially - proc will create as needed CREATE PARTITION FUNCTION PF_MyPartitionFunction(datetime) AS RANGE LEFT FOR VALUES(); GO CREATE PARTITION SCHEME PS_MyPartitionScheme AS PARTITION PF_MyPartitionFunction ALL TO ([PRIMARY]); GO CREATE TABLE dbo.MyPartitionedTable ( PartitionColumnDateTime datetime ) ON PS_MyPartitionScheme(PartitionColumnDateTime); GO --note staging table uses same partition scheme as primary table CREATE TABLE dbo.MyPartitionedTable_Staging ( PartitionColumnDateTime datetime ) ON PS_MyPartitionScheme(PartitionColumnDateTime); GO |
The sliding window proc:
CREATE PROC dbo.SlideRangeLeftWindow_datetime @RetentionDays int, @RunDate datetime = NULL /* This proc maintains a RANGE LEFT daily sliding window based on the specified @RetentionDays. In addition to purging old data, the partition function is adjusted to account for scheduling issues or changes in @RetentionDays. Partitions are split and merged so that the first partition contains the oldest data and the last two partitions (future data) are empty. The other petitions will contain historical and current data for the specified number of @RetentionDays. After successful execution, (at least) the following partitions will exist: - partition 1 = oldest data period - third from last partition = current data (@RunDate) - second from last partition = @RunDate + 1 (normally empty) - last partition = @RunDate + 2 (normally empty) */ AS SET NOCOUNT, XACT_ABORT ON; DECLARE @Error int, @RowCount bigint, @ErrorLine int, @Message varchar(255), @ExpirationDate datetime, @PartitionBoundaryDate datetime; SET @Error = 0; BEGIN TRY IF @RunDate IS NULL BEGIN --use current date (midnight) if no date specified SET @RunDate = DATEADD(day, 0, DATEDIFF(day, '', GETDATE())); END ELSE BEGIN --set time to midnight of specified date SET @RunDate = DATEADD(day, 0, DATEDIFF(day, '', @RunDate)); END SET @Message = 'Run date = ' + CONVERT(varchar(23), @RunDate, 121) + ', Retention days = ' + CAST(@RetentionDays AS varchar(10)); RAISERROR (@Message, 0, 1) WITH NOWAIT; --set datetime boundary to latest posible datetime of previous day SET @RunDate = DATEADD(millisecond, -3, @RunDate); --calculate expiration date based on @RetentionDays and @RunDate SET @ExpirationDate = DATEADD(day, @RetentionDays * -1, @RunDate); RAISERROR ('Partition boundaries before maintenance:', 0, 1) WITH NOWAIT; SELECT CAST(prv.value AS datetime) AS PartitionBoundary FROM sys.partition_functions AS pf JOIN sys.partition_range_values AS prv ON prv.function_id = pf.function_id WHERE pf.name = 'PF_MyPartitionFunction'; --get oldest existing boundary SELECT @PartitionBoundaryDate = CAST(MIN(prv.value) AS datetime) FROM sys.partition_functions AS pf JOIN sys.partition_range_values AS prv ON prv.function_id = pf.function_id WHERE pf.name = 'PF_MyPartitionFunction'; BEGIN TRAN; --acquire exclusive table lock to prevent deadlocking --with concurrent activity. SELECT TOP 1 @error = 0 FROM dbo.MyPartitionedTable WITH (TABLOCKX, HOLDLOCK); --If first partition boundary is later than expiration date, --split first partition so that it contains only expired data. IF @PartitionBoundaryDate > @ExpirationDate OR @PartitionBoundaryDate IS NULL BEGIN SET @PartitionBoundaryDate = @ExpirationDate; ALTER PARTITION SCHEME PS_MyPartitionScheme NEXT USED [PRIMARY]; ALTER PARTITION FUNCTION PF_MyPartitionFunction() SPLIT RANGE(@PartitionBoundaryDate); SET @Message = 'Created boundary ' + CONVERT(varchar(30), @PartitionBoundaryDate, 121); RAISERROR(@Message, 0, 1) WITH NOWAIT; END; --Switch and merge partitions older that retention period WHILE @PartitionBoundaryDate <= @ExpirationDate BEGIN --Ensure target staging partition is empty TRUNCATE TABLE dbo.MyPartitionedTable_Staging; ALTER TABLE dbo.MyPartitionedTable SWITCH PARTITION 1 TO dbo.MyPartitionedTable_Staging PARTITION 1; --get count of purged rows for message SELECT @RowCount = rows FROM sys.partitions WHERE object_id = OBJECT_ID(N'MyPartitionedTable_Staging') AND partition_number = 1; --purge data permanently TRUNCATE TABLE dbo.MyPartitionedTable_Staging; SET @Message = 'Moved data older than ' + CONVERT(varchar(23), @PartitionBoundaryDate, 121) + ' (' + CAST(@RowCount as varchar(20)) + ' rows) to staging table'; RAISERROR(@Message, 0, 1) WITH NOWAIT; --merge first and second partitions ALTER PARTITION FUNCTION PF_MyPartitionFunction() MERGE RANGE(@PartitionBoundaryDate); SET @Message = 'Removed boundary ' + CONVERT(varchar(30), @PartitionBoundaryDate, 121); RAISERROR(@Message, 0, 1) WITH NOWAIT; --get oldest partition boundary for next iteration SET @PartitionBoundaryDate = NULL; SELECT @PartitionBoundaryDate = CAST(MIN(prv.value) AS datetime) FROM sys.partition_functions AS pf JOIN sys.partition_range_values AS prv ON prv.function_id = pf.function_id WHERE pf.name = 'PF_MyPartitionFunction'; END; --Make sure we have individual partitons for tomorrow --and future days. SET @PartitionBoundaryDate = DATEADD(day, 1, @RunDate); WHILE @PartitionBoundaryDate <= DATEADD(day, 2, @RunDate) BEGIN IF NOT EXISTS ( SELECT prv.value FROM sys.partition_functions AS pf JOIN sys.partition_range_values AS prv ON prv.function_id = pf.function_id WHERE pf.name = 'PF_MyPartitionFunction' AND CAST(prv.value AS datetime) = @PartitionBoundaryDate ) BEGIN ALTER PARTITION SCHEME PS_MyPartitionScheme NEXT USED [PRIMARY]; ALTER PARTITION FUNCTION PF_MyPartitionFunction() SPLIT RANGE(@PartitionBoundaryDate); SET @Message = 'Created boundary ' + CONVERT(varchar(30), @PartitionBoundaryDate, 121); RAISERROR(@Message, 0, 1) WITH NOWAIT; END; ELSE BEGIN SET @Message = 'Partition already exists for boundary ' + CONVERT(varchar(30), @PartitionBoundaryDate, 121); RAISERROR(@Message, 0, 1) WITH NOWAIT; END; --calc boundary date for next iteration SET @PartitionBoundaryDate = DATEADD(day, 1, @PartitionBoundaryDate); END COMMIT; END TRY BEGIN CATCH SELECT @Error = ERROR_NUMBER(), @Message = ERROR_MESSAGE(), @ErrorLine = ERROR_LINE(); SET @Message = 'Partition maintenenace failed with error %d at line %d: ' + @Message; RAISERROR(@Message, 16, 1, @Error, @ErrorLine) WITH NOWAIT; IF @@TRANCOUNT > 0 BEGIN ROLLBACK; END; END CATCH; RAISERROR ('Partition boundaries after maintenance', 0, 1) WITH NOWAIT; SELECT CAST(prv.value AS datetime) AS PartitionBoundary FROM sys.partition_functions AS pf JOIN sys.partition_range_values AS prv ON prv.function_id = pf.function_id WHERE pf.name = 'PF_MyPartitionFunction'; Done: RETURN @error; GO |
Demo scripts:
--day 1: create initial 3 partitions INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-01T01:00:00'); EXEC dbo.SlideRangeLeftWindow_datetime @RetentionDays = 1, @RunDate = '2008-09-01T00:00:00'; INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-01T02:00:00'); --day 2: create partition for future data (4 partitions) INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-02T01:00:00'); EXEC dbo.SlideRangeLeftWindow_datetime @RetentionDays = 1, @RunDate = '2008-09-02T00:00:00'; INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-02T02:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-02T03:00:00'); --day 3: purge data and create partition for future data (rolling 4 partitions) INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-03T01:00:00'); EXEC dbo.SlideRangeLeftWindow_datetime @RetentionDays = 1, @RunDate = '2008-09-03T00:00:00'; INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-03T02:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-03T03:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-03T04:00:00'); --day 5: catch-up after missed day 4 INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-04T01:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-04T02:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-04T03:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-04T04:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-04T05:00:00'); INSERT INTO dbo.MyPartitionedTable VALUES('2008-09-05T01:00:00'); EXEC dbo.SlideRangeLeftWindow_datetime @RetentionDays = 1, @RunDate = '2008-09-05T00:00:00'; |
Below is the output from the demo script:
Run date = 2008-09-01 00:00:00.000, Retention days = 1 Partition boundaries before maintenance: PartitionBoundary ----------------------- Created boundary 2008-08-30 23:59:59.997 Moved data older than 2008-08-30 23:59:59.997 (0 rows) to staging table Removed boundary 2008-08-30 23:59:59.997 Created boundary 2008-09-01 23:59:59.997 Created boundary 2008-09-02 23:59:59.997 Partition boundaries after maintenance PartitionBoundary ----------------------- 2008-09-01 23:59:59.997 2008-09-02 23:59:59.997 |
Run date = 2008-09-02 00:00:00.000, Retention days = 1 Partition boundaries before maintenance: PartitionBoundary ----------------------- 2008-09-01 23:59:59.997 2008-09-02 23:59:59.997 Created boundary 2008-08-31 23:59:59.997 Moved data older than 2008-08-31 23:59:59.997 (0 rows) to staging table Removed boundary 2008-08-31 23:59:59.997 Partition already exists for boundary 2008-09-02 23:59:59.997 Created boundary 2008-09-03 23:59:59.997 Partition boundaries after maintenance PartitionBoundary ----------------------- 2008-09-01 23:59:59.997 2008-09-02 23:59:59.997 2008-09-03 23:59:59.997 |
Run date = 2008-09-03 00:00:00.000, Retention days = 1 Partition boundaries before maintenance: PartitionBoundary ----------------------- 2008-09-01 23:59:59.997 2008-09-02 23:59:59.997 2008-09-03 23:59:59.997 Moved data older than 2008-09-01 23:59:59.997 (2 rows) to staging table Removed boundary 2008-09-01 23:59:59.997 Partition already exists for boundary 2008-09-03 23:59:59.997 Created boundary 2008-09-04 23:59:59.997 Partition boundaries after maintenance PartitionBoundary ----------------------- 2008-09-02 23:59:59.997 2008-09-03 23:59:59.997 2008-09-04 23:59:59.997 |
Run date = 2008-09-05 00:00:00.000, Retention days = 1 Partition boundaries before maintenance: PartitionBoundary ----------------------- 2008-09-02 23:59:59.997 2008-09-03 23:59:59.997 2008-09-04 23:59:59.997 Moved data older than 2008-09-02 23:59:59.997 (3 rows) to staging table Removed boundary 2008-09-02 23:59:59.997 Moved data older than 2008-09-03 23:59:59.997 (4 rows) to staging table Removed boundary 2008-09-03 23:59:59.997 Created boundary 2008-09-05 23:59:59.997 Created boundary 2008-09-06 23:59:59.997 Partition boundaries after maintenance PartitionBoundary ----------------------- 2008-09-04 23:59:59.997 2008-09-05 23:59:59.997 2008-09-06 23:59:59.997 |
Legacy Comments
peleg
2008-12-24 |
re: Automating Sliding Window Maintenance i have an un paratiotioned index, and when i try to make a switch i get : "Partition maintenenace failed with error 7733 at line 106: 'ALTER TABLE SWITCH' statement failed. The table 'NReports5.dbo.TargetTable2' is partitioned while index 'IX_TargetTable1_RecipientSender' is not partitioned." what can i do?do i need to fix it on both original table and staging table? thnaks in advance peleg |
Dan Guzman
2009-01-03 |
re: Automating Sliding Window Maintenance This error means that the table is partitioned but the index listed in the error message is not. One of the requirements in order to use SWITCH is that a table and it's indexes must be aligned (partitioned similarly). This implies that the tables cannot contain a mix of partitioned and non-partitioned indexes. See the Books Online for details. The script below (based on DDL from your separate email) shows how you can change the existing non-partitioned index to a partitioned one so that up can use SWITCH: CREATE TABLE [dbo].[TargetTable2_Staging]( [StartDate] [varchar](15) NOT NULL, [Column1] [varchar](25) NOT NULL, [Column2] [varchar](25) NOT NULL, [id] [bigint] IDENTITY(1,1) NOT NULL, CONSTRAINT [PK_TargetTable2_Staging] PRIMARY KEY CLUSTERED ( [StartDate] ASC, [id] ASC ) ON [PRIMARY] ) ON [PRIMARY] GO CREATE NONCLUSTERED INDEX [IX_TargetTable2_Staging] ON [dbo].[TargetTable2_Staging] ( Column1, Column2 ) ON [PRIMARY] GO CREATE PARTITION FUNCTION [PF_TargetTable2](varchar(15)) AS RANGE LEFT FOR VALUES (N'20081210000000', N'20081211000000', N'20081212000000', N'20081213000000', N'20081214000000', N'20081215000000', N'20081216000000', N'20081217000000', N'20081218000000', N'20081230000000') GO CREATE PARTITION SCHEME [PS_TargetTable2] AS PARTITION [PF_TargetTable2] TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]) GO CREATE TABLE [dbo].[TargetTable2]( [StartDate] [varchar](15) NOT NULL, [Column1] [varchar](25) NOT NULL, [Column2] [varchar](25) NOT NULL, [id] [bigint] IDENTITY(1,1) NOT NULL, CONSTRAINT [PK_TargetTable2] PRIMARY KEY CLUSTERED ( [StartDate] ASC, [id] ASC ) ON [PS_TargetTable2]([StartDate]) ) ON [PS_TargetTable2]([StartDate]) GO CREATE NONCLUSTERED INDEX [IX_TargetTable2] ON [dbo].[TargetTable2] ( Column1, Column2 ) ON [PRIMARY] GO --this will fail ALTER TABLE dbo.TargetTable2 SWITCH PARTITION 1 TO TargetTable2_Staging /* Msg 7733, Level 16, State 4, Line 1 'ALTER TABLE SWITCH' statement failed. The table 'TechnologyStack.dbo.TargetTable2' is partitioned while index 'IX_TargetTable2' is not partitioned. */ --recreate source table index as partitioned CREATE NONCLUSTERED INDEX [IX_TargetTable2] ON [dbo].[TargetTable2] ( Column1, Column2 ) WITH (DROP_EXISTING = ON) ON [PS_TargetTable2]([StartDate]) --this will succeed ALTER TABLE dbo.TargetTable2 SWITCH PARTITION 1 TO TargetTable2_Staging |