CHDS
Clément HugéClément Hugé

Is Your Large SQL Server Database Slowing Down Your Business? Learn How to Master rangeId Partitioning! 🚀

This article discusses a hot topic in database administration and how to automate partitioning with ever-increasing ids.

automatepartsqlserver

Is Your Large SQL Server Database Slowing Down Your Business? Learn How to Master rangeId Partitioning! 🚀


If your large OLTP databases are causing migraines—think re-indexation jobs taking over 20 hours or purge jobs that can't keep up with inserts—it's time for a strategic intervention. Your system's 24/7 activity demands a solution that maintains performance without downtime. The answer? PARTITIONING.

But for massive tables, using a non-partitioning key can lead to immense index storage overhead. That's where rangeId partitioning on a clustered identity column is a game-changer. It maintains your indexes' compact alignment while dramatically improving maintenance efficiency.

The challenge is often the management itself: When to prune? How to extend? What if the scheduled job fails?

Fear not—with a systematic, expert-driven approach, you can successfully implement robust, scalable partitioning.



The Power of a Systematic Approach


For large, transactional environments, partitioning is not a one-time fix; it's a new maintenance paradigm. The key to long-term success lies in adopting a generic and systematic approach using custom functions and stored procedures.


Why Automation Matters:


  • Consistency: Many large tables often share the same partition scheme, requiring uniform handling.
  • Reduced Risk: Automating highly technical tasks significantly decreases the likelihood of human error and uncertainty.
  • Scalability: A generic framework is easier to improve, adapt, and extend as your data needs evolve.

This approach will solve the most painful issues currently plaguing your database operations:

Current Pain Point

  • Indexes cannot be reindexed due to excessive size/resource demands.
  • Partition maintenance handles smaller, manageable data chunks.
  • Purge mechanisms are slow, cause high CPU, and can't keep up with inserts.
  • Stakeholders (CTO/CEO) worry about rising costs and decreasing performance.

Partitioning Solution

  • Truncating entire cold partitions is near-instantaneous and low-impact.
    • Transaction logs are massive from re-indexation and purges.
    • Truncation is minimally logged, shrinking log files and backup sizes.
  • Improved performance and optimized storage/backup costs bring immediate relief.


Choosing the Right Architecture: Ever-Increasing Range Partitioning


Before implementation, you must deeply understand your data pattern—the lifecycle of your data from hot (frequent DML) to semi-cold (read-only) to cold (archivable).

While Rolling Window Partitioning (based on a computed date column) is an option, it requires adding the partition key to all indexes, demanding significant storage and structural changes.

For minimal index storage impact and maximum efficiency on large OLTP tables, we prefer the Ever-Increasing Range Partitioning based on the Identity (Id) column.


Advantages of Id-Range Partitioning:


  1. Index Alignment: Indexes stay identically aligned without requiring additional storage—a critical advantage for multi-terabyte tables.
  2. Consistent Partition Size: Using an Id range based on row count allows for uniform partition sizes, simplifying maintenance and query optimization.
  3. Fast Pruning: We use the date column (via a separate query) only to determine the minimum Id to keep, allowing the use of the TRUNCATE TABLE ... WITH (PARTITIONS) command for near-instant deletion of cold data.

The main drawback is the need for scheduling maintenance to merge, extend, and prune but is fully mitigated by implementing the following generic stored procedures.



Implementation: The Core Automation Logic


A successful rangeId implementation relies on two core, dynamic stored procedures, supported by utility functions to manage dependencies:


1. Utility Functions


  • [dbo].[GetFileGroupFromTable]: Dynamically determines the filegroup for the partitioned table.
  • [dbo].[FkScript]: Generates dynamic SQL to safely DROP and CREATE/CHECK foreign key constraints surrounding the partitioning operations.
  • [dbo].[IndexPartition]: View that provides the partition scheme and function for any index (and more, like the row count and boundary value).
CREATE OR ALTER   VIEW [dbo].[IndexPartition]
AS
SELECT DISTINCT sc.name AS SchemaName,
                t.name  AS TableName,
                i.Name  AS IndexName,
                s.name  aS PScheme,
                f.name  aS PFunction,
                p.partition_number,
                r.value AS [Boundary Value],
                st.row_count
FROM sys.tables AS t
         JOIN sys.schemas AS sc
              ON t.schema_id = sc.schema_id
         JOIN sys.indexes AS i
              ON t.object_id = i.object_id
         JOIN sys.partitions AS p
              ON i.object_id = p.object_id AND i.index_id = p.index_id
         JOIN sys.partition_schemes AS s
              ON i.data_space_id = s.data_space_id
         JOIN sys.partition_functions AS f
              ON s.function_id = f.function_id
         LEFT JOIN sys.partition_range_values AS r
                   ON f.function_id = r.function_id and r.boundary_id = p.partition_number
         LEFT JOIN sys.dm_db_partition_stats st
                   ON st.partition_number = p.partition_number
                       AND OBJECT_ID(t.name) = st.object_id
GO
CREATE OR ALTER     FUNCTION [dbo].[GetFileGroupFromTable]
(@SchemaName sysname, @TableName sysname)
    RETURNS SYSNAME
AS
BEGIN

    RETURN
        (
            SELECT TOP 1	fg.name as [Filegroup]
            FROM sys.partition_functions AS pf
                     JOIN sys.partition_schemes as ps on ps.function_id=pf.function_id
                     JOIN sys.indexes as si on si.data_space_id=ps.data_space_id
                     JOIN sys.objects as so on si.object_id = so.object_id
                     JOIN sys.schemas as sc on so.schema_id = sc.schema_id
                     JOIN sys.partitions as p on
                si.object_id=p.object_id
                    and si.index_id=p.index_id
                     LEFT JOIN sys.partition_range_values as prv on prv.function_id=pf.function_id
                and p.partition_number=
                    CASE pf.boundary_value_on_right WHEN 1
                                                        THEN prv.boundary_id + 1
                                                    ELSE prv.boundary_id
                        END
                /* For left-based functions, partition_number = boundary_id,
                   for right-based functions we need to add 1 */
                     JOIN sys.dm_db_partition_stats as stat on stat.object_id=p.object_id
                and stat.index_id=p.index_id
                and stat.index_id=p.index_id and stat.partition_id=p.partition_id
                and stat.partition_number=p.partition_number
                     JOIN sys.allocation_units as au on au.container_id = p.hobt_id
                and au.type_desc ='IN_ROW_DATA'
                /* Avoiding double rows for columnstore indexes. */
                /* We can pick up LOB page count from partition_stats */
                     JOIN sys.filegroups as fg on fg.data_space_id = au.data_space_id
            WHERE so.[Name] = @TableName
              AND sc.[name] = @SchemaName
        )
END

GO

CREATE OR ALTER         PROCEDURE [dbo].[FkScript]
@TableList NVARCHAR(MAX) = 'ConsumerSession,dbo.Event'
AS

;
WITH CTE AS
         (
		SELECT	DISTINCT
				REPLACE(REPLACE(CASE WHEN CHARINDEX('.',Value) = 0 THEN 'dbo' ELSE LEFT(Value,CHARINDEX('.',Value)-1) END,'[',''),']','') as SchemaName,
				REPLACE(REPLACE(CASE WHEN CHARINDEX('.',Value) = 0 THEN Value ELSE RIGHT(Value,LEN(Value)-CHARINDEX('.',Value)) END,'[',''),']','') as TableName 
		FROM string_split(@TableList, ',') A
		 ),
	CTEIds AS 
		(
		SELECT A.*, B.Object_id, C.schema_id 
		FROM CTE A
		JOIN sys.tables B ON A.[TableName] = B.[Name]
		JOIN sys.schemas C ON B.schema_id = C.schema_id AND C.[Name] = A.SchemaName
		)

SELECT CONVERT(SMALLINT,1) AS OrderId, 'DROP' ScriptType, '
IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE [Name] = '''+fk.Name+''')
ALTER TABLE '+QUOTENAME(sch2.[Name])+'.'+QUOTENAME(OBJECT_NAME(fk.parent_object_id))+' DROP CONSTRAINT ' +QUOTENAME(fk.Name)+ ';' Script
FROM sys.foreign_keys fk
JOIN CTEIds ON fk.referenced_object_id = CTEIds.Object_Id
JOIN sys.tables tb ON fk.referenced_object_id = tb.object_id
JOIN sys.schemas sch ON sch.schema_id = tb.schema_id
JOIN sys.tables tb2 ON fk.parent_object_id = tb2.object_id
JOIN sys.schemas sch2 ON sch2.schema_id = tb2.schema_id

UNION ALL

SELECT CONVERT(SMALLINT,2) AS OrderId, 'CREATE', '
IF NOT EXISTS (SELECT 1 FROM sys.foreign_keys WHERE [Name] = '''+fk.Name+''')
ALTER TABLE '+QUOTENAME(sch2.[Name])+'.'+QUOTENAME(OBJECT_NAME(fk.parent_object_id))+' WITH NOCHECK ADD CONSTRAINT ' +QUOTENAME(fk.Name)
    + ' FOREIGN KEY('+QUOTENAME(sc.name)+')'
    + ' REFERENCES '+QUOTENAME(sch.[Name])+'.'+QUOTENAME(OBJECT_NAME(fk.referenced_object_id))+'('+QUOTENAME(tc.name)+') ;'
FROM sys.foreign_keys fk
JOIN CTEIds ON fk.referenced_object_id = CTEIds.Object_Id
JOIN sys.tables tb ON fk.referenced_object_id = tb.object_id
JOIN sys.schemas sch ON sch.schema_id = tb.schema_id
JOIN sys.tables tb2 ON fk.parent_object_id = tb2.object_id
JOIN sys.schemas sch2 ON sch2.schema_id = tb2.schema_id
         JOIN sys.foreign_key_columns fkc ON fkc.constraint_object_id = fk.object_id
         JOIN sys.columns sc ON sc.column_id = fkc.parent_column_id AND sc.object_id = fk.parent_object_id
         JOIN sys.columns tc ON tc.column_id = fkc.referenced_column_id AND tc.object_id = fk.referenced_object_id

UNION ALL


SELECT CONVERT(SMALLINT,3) AS OrderId, 'CHECK', '
IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE [Name] = '''+fk.Name+''')
ALTER TABLE '+QUOTENAME(sch2.[Name])+'.'+QUOTENAME(OBJECT_NAME(fk.parent_object_id))+' CHECK CONSTRAINT ' +QUOTENAME(fk.Name)+ ';'
FROM sys.foreign_keys fk
JOIN CTEIds ON fk.referenced_object_id = CTEIds.Object_Id
JOIN sys.tables tb ON fk.referenced_object_id = tb.object_id
JOIN sys.schemas sch ON sch.schema_id = tb.schema_id
JOIN sys.tables tb2 ON fk.parent_object_id = tb2.object_id
JOIN sys.schemas sch2 ON sch2.schema_id = tb2.schema_id

GO


2. Maintenance Stored Procedures


Procedure: [dbo].[MergeExtendPartitions]

Purpose: Manages the growth and consolidation of partitions.

Key features:

  • Extends the partition function by a set partitionSize (e.g., 10 million rows) to ensure a predetermined number of empty partitions (e.g., 10) are always available for incoming inserts. It also
  • Merges old, empty partitions at the front to maintain a clean structure.

Here is the example of extending 10 empty partitions to a table with such procedure:

DECLARE 
	@SchemaName SYSNAME = 'Schema', 
	@TableName SYSNAME = 'TableToMergeAndExtend',
	@pscheme VARCHAR(400), 
	@pfunction VARCHAR(400), 
	@Filegroup VARCHAR(400),
	@partitionSize int=10000000,
	@partitionsToMaintain int=10, 
	@ProduceScript bit=0
SELECT @Filegroup = [dbo].[GetFileGroupFromTable](@SchemaName,@TableName)
SELECT TOP 1 @pscheme = PScheme, @pFunction = PFunction 
FROM dbo.IndexPartition
WHERE TableName = @TableName
AND SchemaName = @SchemaName
AND IndexName LIKE 'PK%'  
PRINT 
'	
EXEC dbo.MergeExtendPartitions 
		@pscheme='''+@pscheme+''', @pfunction='''+@pfunction+''', @Tablename='''+@Tablename+''',
		@Filegroup='''+@Filegroup+''',@partitionSize='+convert(varchar,@partitionSize)+',@partitionsToMaintain='+convert(varchar,@partitionsToMaintain)+', 
		@ProduceScript='+convert(varchar,@ProduceScript)+'
'
EXEC dbo.MergeExtendPartitions 
	@pscheme=@pscheme, @pfunction=@pfunction, @Tablename=@Tablename,
	@Filegroup=@Filegroup,@partitionSize=@partitionSize,@partitionsToMaintain=@partitionsToMaintain, 
	@ProduceScript=@ProduceScript
GO


Procedure: [dbo].[Maintenance_PurgeTable]

Purpose: Deletes obsolete data using fast partition truncation.

Key features:

  • Calculates the minimum Id to keep based on a MonthRetention rule (e.g., 14 months) and a custom query.
  • It then dynamically drops FKs, truncates all obsolete partitions, and recreates the FKs—all within a single, fast transaction.

Here is the example of keeping a table with only 14 month using a specific query to find the minimum id to keep:

DECLARE 
	@MinIdSelect NVARCHAR(MAX) = 'SELECT Min(Id) FROM Schema.[TableToMergeAndExtend] WHERE [CreatedDate] > @Date',
    @MonthRetention SMALLINT= 14, 
	@Debug BIT = 0,
	@MinIdOutput BIGINT = 0
EXEC [dbo].[Maintenance_PurgeTable] 
	@SchemaName = 'dbo', 
	@TableName = 'MachineConnectivityHistory',
	@MinIdSelect = @MinIdSelect,
    @MonthRetention = @MonthRetention, 
	@Debug = @Debug,
	@MinIdInput = @MinIdOutput,
	@MinIdOutput = @MinIdOutput OUTPUT

GO


Here are the stored procedures used for implementation:

CREATE OR ALTER       PROCEDURE [dbo].[MergeExtendPartitions]
    @pscheme VARCHAR(400), @pfunction VARCHAR(400), @Tablename VARCHAR (400), @Filegroup VARCHAR (400),
    @partitionSize INT, @partitionsToMaintain INT, @ProduceScript BIT = 0
AS
    -- SET NOCOUNT ON added to prevent extra result sets from
    SET NOCOUNT ON;

--Variable Declaration
DECLARE
    @highestDataPartition INT
    , @lowestDataPartition INT
    , @highestPartition INT
    , @highestPartValue BIGINT
    , @lowestPartition INT
    --, @partitionSize INT
    , @partValue BIGINT
    --, @partitionsToMaintain INT
    , @partitionsToKeep INT --currently set to keep only one partition in front, but can be changed.
    , @availablePartitions INT
    , @partitionsToCreate INT
    , @sql NVARCHAR(4000)
    , @DatabaseName NVARCHAR(50) = DB_NAME()
    , @Task NVARCHAR(50)
    , @StartTime DATETIME = GETDATE()
    --, @pscheme VARCHAR(400)
    --, @pfunction VARCHAR(400)
    --, @Tablename VARCHAR (400)
    --, @Filegroup VARCHAR (400)
    ;

    --Configure this
--SET @pscheme = 'ps_Prod2_Item' ; --partition function
--SET @pfunction = 'pf_Prod2_Item'; --partition scheme
--SET @Tablename = 'Item'
--SET @Filegroup = 'Item'
--SET @partitionSize = 6000000 ; --40 Million - Preset Partition Size/Chunk
--SET @partitionsToMaintain = 50 ; --Number of Partitions to maintain any given time

--Delete temp table if exists
    DROP TABLE IF EXISTS #Partitions

--Populate Temp Table #Partitions
SELECT
    OBJECT_NAME(i.[object_id]) AS [ObjectName],
    i.index_id,
    prv_left.value AS LowerBoundaryValue,
    prv_right.value AS UpperBoundaryValue,
    CASE pf.boundary_value_on_right
        WHEN 1
            THEN 'RIGHT'
        ELSE 'LEFT'
        END AS PartitionFunctionRange,
    p.partition_number AS PartitionNumber,
    p.rows AS Rows
INTO #Partitions
FROM sys.partitions AS p
         INNER JOIN sys.indexes AS i ON i.object_id = p.object_id AND i.index_id = p.index_id
         INNER JOIN sys.data_spaces AS ds ON ds.data_space_id = i.data_space_id
         INNER JOIN sys.partition_schemes AS ps ON ps.data_space_id = ds.data_space_id
         INNER JOIN sys.partition_functions AS pf ON pf.function_id = ps.function_id
         LEFT OUTER JOIN sys.partition_range_values AS prv_left ON ps.function_id = prv_left.function_id AND prv_left.boundary_id = p.partition_number - 1
         LEFT OUTER JOIN sys.partition_range_values AS prv_right ON ps.function_id = prv_right.function_id AND prv_right.boundary_id = p.partition_number
WHERE ds.name = @pscheme --Partition Scheme Name
  AND i.index_id=1
  AND OBJECT_NAME(i.[object_id]) = @Tablename

    --SELECT * FROM #Partitions ORDER BY PartitionNumber
--DROP TABLE #Partitions

--Set variables for extention
    SET @highestDataPartition = (SELECT MAX(PartitionNumber) FROM #Partitions WHERE Rows <> 0);
    SET @availablePartitions = ((SELECT MAX( PartitionNumber ) FROM #Partitions) - @highestDataPartition) - 1 /* Minus 1 excluding last partition with no upper boundry value*/ ;
    SET @partitionsToCreate = @partitionsToMaintain - @availablePartitions ;
    SET @highestPartition = ((SELECT MAX( PartitionNumber ) FROM #Partitions) - 1) ; /* Minus 1 excluding last partition with no upper boundry value*/
    SET @highestPartValue = (SELECT CONVERT(VARCHAR(12),UpperBoundaryValue,112)FROM #Partitions WHERE PartitionNumber = @highestPartition) ;

--Set variables for merging
    SET @lowestPartition = (SELECT MIN(PartitionNumber) FROM #Partitions) ;
    SET @lowestDataPartition = (SELECT MIN(PartitionNumber) FROM #Partitions WHERE Rows <> 0);
    SET @partitionsToKeep = (@lowestDataPartition - 1); --First partition with data minus 1 keep one empty partition in front of it

--Generate commands for New Partitions
BEGIN TRY
    IF @partitionsToMaintain = @availablePartitions OR @partitionsToMaintain < @availablePartitions
        BEGIN
            PRINT 'No Action Needed. Available Partitions is: ' + CONVERT( VARCHAR(MAX), @availablePartitions ) + ' and Number of Partition to Maintain is: ' + CONVERT( VARCHAR(MAX), @partitionsToMaintain ) ;
        END ;
    ELSE
        BEGIN
            SET @partValue = CAST(@highestPartValue as VARCHAR(12));
            SET @Task = 'Extend Partition'
            WHILE @partitionsToCreate > 0
                BEGIN
                    SET @partValue = @partValue + @partitionSize ; -- Partition Boundary Value Plus @partitionSize
                    SET @sql = 'ALTER PARTITION SCHEME ' + QUOTENAME(@pscheme) + 'NEXT USED ' + QUOTENAME(@Filegroup) + '
			 ALTER PARTITION FUNCTION ' + QUOTENAME(@pfunction) + '() SPLIT RANGE (' + CONVERT(VARCHAR(12), @partValue, 112) + ')' ;

                    BEGIN
                        IF @ProduceScript = 1
                            SELECT @sql
                        ELSE
                            EXEC (@sql);
                        --PRINT @sql;
                    END
                    SET @partitionsToCreate = @partitionsToCreate - 1 ;
                END
        END
END TRY

BEGIN CATCH

    DECLARE @ErrorNumber int;
    DECLARE @ErrorMessage nvarchar(250);
    DECLARE @ErrorSeverity int;
    DECLARE @ErrorState int;
    DECLARE @ErrorLine int;

    SELECT	 @ErrorNumber = ERROR_NUMBER()
         ,@ErrorMessage = ERROR_MESSAGE()
         ,@ErrorSeverity = ERROR_SEVERITY()
         ,@ErrorState = ERROR_STATE()
         ,@ErrorLine = ERROR_LINE();

    RAISERROR(@ErrorMessage,@ErrorSeverity,@ErrorState,@ErrorLine);

END CATCH


--Generate commands to merge old partitions
BEGIN TRY
    IF @lowestPartition >= (@lowestDataPartition - 1)
        BEGIN
            PRINT 'No Action Needed. Current Lowest Partition with Data is: ' + CONVERT(VARCHAR(MAX), @lowestDataPartition) + ' and lowest partition is: ' + CONVERT(VARCHAR(MAX), @lowestPartition);
        END ;
    ELSE
        BEGIN
            SET @Task = 'Merge Partition'
            WHILE @partitionsToKeep > 1
                BEGIN
                    SET @partValue = (SELECT CONVERT(VARCHAR(12),UpperBoundaryValue,112)FROM #Partitions WHERE PartitionNumber = @lowestPartition) ;
                    SET @sql = 'ALTER PARTITION FUNCTION ' + QUOTENAME(@pfunction) + '() MERGE RANGE (' + CONVERT(VARCHAR(12), @partValue, 112 ) + ')' ;

                    BEGIN
                        IF @ProduceScript = 1
                            SELECT @sql
                        ELSE
                            EXEC (@sql);
                        --PRINT @sql
                    END
                    SET @partitionsToKeep = @partitionsToKeep - 1 ;
                    SET @lowestPartition = @lowestPartition +1
                END ;
        END
END TRY

BEGIN CATCH
    SELECT	 @ErrorNumber = ERROR_NUMBER()
         ,@ErrorMessage = ERROR_MESSAGE()
         ,@ErrorSeverity = ERROR_SEVERITY()
         ,@ErrorState = ERROR_STATE()
         ,@ErrorLine = ERROR_LINE();

    RAISERROR(@ErrorMessage,@ErrorSeverity,@ErrorState,@ErrorLine);
END CATCH

--Cleanup
    DROP TABLE IF EXISTS #Partitions ;

GO

CREATE OR ALTER     PROCEDURE [dbo].[Maintenance_PurgeTable]
    @SchemaName SYSNAME,
    @TableName SYSNAME,
    @MinIdSelect NVARCHAR(MAX),
    @MonthRetention TINYINT = 14,
    @Debug BIT = 1,
    @MinIdInput BIGINT = 0,
    @MinIdOutput BIGINT OUTPUT

AS

    SET NOCOUNT ON;
BEGIN TRY
    SET @MonthRetention = ISNULL(@MonthRetention,14)
    --IF @MonthRetention > 10 OR @MonthRetention < 1
    --    RAISERROR('Month retention MUST be between 1 AND 10',16,1)

    -- CALCULATE CS3 Minimum to keep based on ReceivedTime
    DECLARE @Date DATETIME = DATEADD(MONTH, -@MonthRetention, GETDATE())
    --SELECT @Date
    DECLARE @MinCSId BIGINT
    SET @MinCSId = @MinIdInput
    IF @MinCSId IS NULL OR @MinCSId = 0
        BEGIN
            DROP TABLE IF EXISTS #MinId
            CREATE TABLE #MinId (Id BIGINT)
            INSERT INTO #Minid EXEC sp_ExecuteSQL @MinIdSelect, N'@Date DATETIME', @Date
            SELECT TOP 1 @MinCSId = Id FROM #MinId
        END

    IF @MinCSId IS NULL
        RAISERROR('There is no minimum Id',16,1)
    PRINT 'Min id: ' + CONVERT(VARCHAR,@MinCSId)
    SET @MinIdOutput = @MinCSId

    DECLARE @MinPartitionNumberToKeep SMALLINT
    DECLARE @PFunction SYSNAME
    SELECT TOP 1 @PFunction=PFunction FROM dbo.IndexPartition WHERE SchemaName = @SchemaName AND TableName = @TableName
    DECLARE @pfStmt NVARCHAR(MAX)
    SET @pfStmt = 'SELECT Id = $Partition.['+@PFunction+'](@MinCSId)'

    DROP TABLE IF EXISTS #MinPartitionNumberToKeep
    CREATE TABLE #MinPartitionNumberToKeep(Id SMALLINT)
    INSERT INTO #MinPartitionNumberToKeep EXEC sp_ExecuteSQL @pfStmt, N'@MinCSId BIGINT', @MinCSId
    SELECT TOP 1 @MinPartitionNumberToKeep = Id FROM #MinPartitionNumberToKeep
    IF @MinPartitionNumberToKeep IS NULL
        RAISERROR('There is no minimum partition number',16,1)
    PRINT 'Min Partition number not to prune: ' + CONVERT(VARCHAR,@MinPartitionNumberToKeep)

    DECLARE @Stmt NVARCHAR(MAX) = '', @tableList NVARCHAR(MAX) = ''
    SELECT @Stmt = @Stmt + Stmt, @tableList = @tableList + ',' + TableName
    FROM (
             SELECT DISTINCT TableName, '
		TRUNCATE TABLE dbo.'+QUOTENAME(TableName)+' WITH (PARTITIONS ( '+CONVERT(NVARCHAR(16),partition_number)+'));' AS Stmt
             FROM dbo.IndexPartition
             WHERE SchemaName = @SchemaName AND TableName = @TableName
               AND partition_number < @MinPartitionNumberToKeep
               AND row_count > 0
         ) SUB
    DROP TABLE IF EXISTS #FKDependencies
    CREATE TABLE #FKDependencies(ScriptType VARCHAR(32) NOT NULL, Script NVARCHAR(MAX) NOT NULL, OrderId SMALLINT NOT NULL)
    INSERT INTO #FKDependencies (OrderId, ScriptType, Script)
        EXEC dbo.FkScript @TableList = @tableList -- 'ConsumerSession,ConsumerSessionProperty,Item,ItemProperty,ConsumerSessionPayoutReceiverProperty,ConsumerSessionPayoutMethodProperty,ClearingReport_ConsumerSession'

    DECLARE @StmtFkDrop NVARCHAR(MAX) = ''
    SELECT @StmtFkDrop = @StmtFkDrop + Script FROM #FKDependencies WHERE ScriptType = 'DROP'
    --PRINT @StmtFkDrop
    DECLARE @StmtFkCreate NVARCHAR(MAX) = ''
    SELECT @StmtFkCreate = @StmtFkCreate + Script FROM #FKDependencies WHERE ScriptType = 'CREATE'
    --PRINT @StmtFkCreate
    DECLARE @StmtFkCheck NVARCHAR(MAX) = ''
    SELECT @StmtFkCheck = @StmtFkCheck + Script FROM #FKDependencies WHERE ScriptType = 'CHECK'
    --PRINT @StmtFkCheck

    SET @Stmt =
            'BEGIN TRY
            BEGIN TRANSACTION'
                + @StmtFkDrop
                + @Stmt
                + @StmtFkCreate
                + @StmtFkCheck
                +
            '
            COMMIT TRANSACTION
            END TRY
            BEGIN CATCH
            ROLLBACK TRANSACTION
            DECLARE @ErrMsg NVARCHAR(MAX) = ERROR_MESSAGE()
            RAISERROR(@ErrMsg,16,1)
            END CATCH
            '
    PRINT @Stmt
    IF @Debug = 0
        EXEC sp_ExecuteSQL @Stmt

END TRY
BEGIN CATCH
    DECLARE @ErrorMessage NVARCHAR(MAX) = ERROR_MESSAGE()
    RAISERROR(@ErrorMessage,16,1)
END CATCH
GO


This systematic approach, fully encapsulated in the provided T-SQL code, ensures your maintenance is proactive, automated, and minimal-impact.



Migration and Long-Term Success


Implementing partitioning on existing, multi-billion-row tables is not trivial. It requires careful planning to manage large transactions, avoid locking, and monitor performance.

  • Data Optimization: The migration phase is the ideal time to optimize data types and apply data compression—a client's 1.5 billion-row table went from 800 GB to 75 GB after a migration! 🤯
  • No Downtime Migration: With expert assistance, even multi-terabyte tables can be migrated successfully with no downtime, using advanced techniques for seamless data movement.

Once in place, the scheduled jobs for MergeExtendPartitions and Maintenance_PurgeTable run reliably, ensuring a scalable, fast, and low-cost database for years to come.

Need help reclaiming control over your large SQL Server tables? Don't let your data infrastructure become a liability. Partnering with experts can transform an anxious, high-cost, low-performance environment into a streamlined, high-performance asset.

➡️ Want to see how this architecture can be applied to your specific database environment? Reach out to discuss your SQL Server partitioning strategy!