ideas & tech in beta

 

Fast delete using dynamic partition switching in T-SQL

It is impossible to drop a specific partition, from last month for example. Also, as one cant place a where clause on a truncate statement, a delete from table where x=y would be logged and hence very slow. However, one can switch data from a specific partition into a another table and then truncate that table.

The following is a dynamic method of doing this for any table. This sp must be created first as it is called by another sp that does the work. It creates a table with the same schema as the table from which the partition is to be deleted. The data to be deleted is switched into the created table. Note: the switch out table must be permanent (unfortunately you cannot switch to a #tmp table) so one must check for schema locks.

GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author:      Harry McCarney
-- Object: mnt_GetSQLForSwitchOutTable
-- =============================================
ALTER PROCEDURE [dbo].[mnt_GetSQLForSwitchOutTable] 
(
    @TableName SYSNAME,
    @NewTableName SYSNAME = NULL,
    @Output varchar(max) output
)
AS
BEGIN

    DECLARE @MainDefinition TABLE
    (
        FieldValue VARCHAR(200)
    )

    DECLARE @DBName SYSNAME
    DECLARE @ClusteredPK BIT
    DECLARE @TableSchema NVARCHAR(255)
    SET @DBName = DB_NAME(DB_ID())

    SELECT @TableName = name FROM sysobjects WHERE id = OBJECT_ID(@TableName)

    DECLARE @ShowFields TABLE
    (
        FieldID INT IDENTITY(1,1),
        DatabaseName VARCHAR(100),
        TableOwner VARCHAR(100),
        TableName VARCHAR(100),
        FieldName VARCHAR(100),
        ColumnPosition INT,
        ColumnDefaultValue VARCHAR(100),
        ColumnDefaultName VARCHAR(100),
        IsNullable BIT,
        DataType VARCHAR(100),
        MaxLength INT,
        NumericPrecision INT,
        NumericScale INT,
        DomainName VARCHAR(100),
        FieldListingName VARCHAR(110),
        FieldDefinition CHAR(1),
        IdentityColumn BIT,
        IdentitySeed INT,
        IdentityIncrement INT,
        IsCharColumn BIT
    )

    DECLARE @HoldingArea TABLE
    (
        FldID SMALLINT IDENTITY(1,1),
        Flds VARCHAR(4000),
        FldValue CHAR(1) DEFAULT(0)
    )

    DECLARE @PKObjectID TABLE
    (
        ObjectID INT
    )

    DECLARE @Uniques TABLE
    (
        ObjectID INT
    )

    DECLARE @HoldingAreaValues TABLE
    (
        FldID SMALLINT IDENTITY(1,1),
        Flds VARCHAR(4000),
        FldValue CHAR(1) DEFAULT(0)
    )

    DECLARE @Definition TABLE
    (
        DefinitionID SMALLINT IDENTITY(1,1),
        FieldValue VARCHAR(200)
    )


    INSERT INTO @ShowFields
    (       
        DatabaseName,
        TableOwner,
        TableName,
        FieldName,
        ColumnPosition,
        ColumnDefaultValue,
        ColumnDefaultName,
        IsNullable,
        DataType,
        MaxLength,
        NumericPrecision,
        NumericScale,
        DomainName,
        FieldListingName,
        FieldDefinition,
        IdentityColumn,
        IdentitySeed,
        IdentityIncrement,
        IsCharColumn
    )

    SELECT
        DB_NAME(),
        TABLE_SCHEMA,
        TABLE_NAME,
        COLUMN_NAME,
        CAST(ORDINAL_POSITION AS INT),
        COLUMN_DEFAULT,
        dobj.name AS ColumnDefaultName,
        CASE WHEN c.IS_NULLABLE = 'YES' THEN 1 ELSE 0 END,
        DATA_TYPE,
        CAST(CHARACTER_MAXIMUM_LENGTH AS INT),
        CAST(NUMERIC_PRECISION AS INT),
        CAST(NUMERIC_SCALE AS INT),
        DOMAIN_NAME,
        COLUMN_NAME + ',',
        '' AS FieldDefinition,
        CASE WHEN ic.object_id IS NULL THEN 0 ELSE 1 END AS IdentityColumn,
        CAST(ISNULL(ic.seed_value,0) AS INT) AS IdentitySeed,
        CAST(ISNULL(ic.increment_value,0) AS INT) AS IdentityIncrement,
        CASE WHEN st.collation_name IS NOT NULL THEN 1 ELSE 0 END AS IsCharColumn
    FROM
        INFORMATION_SCHEMA.COLUMNS c
        JOIN sys.columns sc ON  c.TABLE_NAME = OBJECT_NAME(sc.object_id) AND c.COLUMN_NAME = sc.Name
        LEFT JOIN sys.identity_columns ic ON c.TABLE_NAME = OBJECT_NAME(ic.object_id) AND c.COLUMN_NAME = ic.Name
        JOIN sys.types st ON COALESCE(c.DOMAIN_NAME,c.DATA_TYPE) = st.name

        LEFT OUTER JOIN sys.objects dobj ON dobj.object_id = sc.default_object_id AND dobj.type = 'D'


    WHERE c.TABLE_NAME = @TableName
    ORDER BY
        c.TABLE_NAME, c.ORDINAL_POSITION

    SELECT TOP 1 @TableSchema = TableOwner
    FROM @ShowFields

    INSERT INTO @HoldingArea (Flds) VALUES('(')

    INSERT INTO @Definition(FieldValue)
    VALUES('CREATE TABLE ' + CASE WHEN @NewTableName IS NOT NULL THEN @NewTableName ELSE @DBName + '.' + @TableSchema + '.' + @TableName END)

    INSERT INTO @Definition(FieldValue)
    VALUES('(')

    INSERT INTO @Definition(FieldValue)
        SELECT
                CHAR(10) + FieldName + ' ' +
                                    CASE
                                        WHEN DomainName IS NOT NULL  THEN DomainName + CASE WHEN IsNullable = 1 THEN ' NULL ' ELSE ' NOT NULL ' END
                                        ELSE UPPER(DataType) +
                                            CASE WHEN IsCharColumn = 1 THEN '(' + CAST(MaxLength AS VARCHAR(10)) + ')' ELSE '' END +
                                            CASE WHEN IdentityColumn = 1 THEN ' IDENTITY(' + CAST(IdentitySeed AS VARCHAR(5))+ ',' + CAST(IdentityIncrement AS VARCHAR(5)) + ')' ELSE '' END +
                                            CASE WHEN IsNullable = 1 THEN ' NULL ' ELSE ' NOT NULL ' END +
    										 CASE WHEN FieldID = (SELECT MAX(FieldID) FROM @ShowFields) THEN '' ELSE ',' END end
        FROM    @ShowFields 

INSERT INTO @MainDefinition(FieldValue)
            SELECT FieldValue FROM @Definition
            ORDER BY DefinitionID ASC


        SELECT @output  = isnull(@output,'') + FieldValue FROM @MainDefinition


END

The next sp calls the sp above and does the deleting. It removes data from partitions older than the number of days spcified in the input paramter. This is much faster than querying the tables and issuing deletes. Using switch and truncate we take no locks and perform no scans/seeks. Specific partitions cant be explicitly dropped/truncated.

We switch data into a difffernet table: Table_SwitchOut and truncate it from there. Switch statements require DDL and we may want to switch multiple partitions This means dynamically running DDL alter table statements which is perhaps not ideal. I have also created dynamic structure in case job fails one night or we only want to run it once a a week.

Also if we stop collecting data it will keep the last @DaysPartitionsToKeep days collected data. The sp checks that the partitions are actually weekly or monthly by querying system tables. This double check ensures we don’t delete data erroneously if the partition function is altered.

-- =============================================
-- Author:      Harry McCarney
-- Object: mnt_Truncate_Data_From_Partitions

-- =============================================

ALTER PROC [dbo].[mnt_Truncate_Data_From_Partitions] 
@Table_Name varchar (100),
@DaysPartitionsToKeep INT ---Number of days data to keep aassuming daily partition. 
AS

SET NOCOUNT ON
SET transaction ISOLATION level read uncommitted

DECLARE @no       INT,
        @i        INT,
        @sql      VARCHAR(MAX),
        @truncate VARCHAR(500),
    	@Table_Name_SwitchOut varchar(100),
    	@now BIGINT,
    	@PartionCheck INT,
    	@PartionScheme varchar(100),
    	@PartitionsToKeep INT,
    	@PartitionFunction varchar(100),
    	@Error varchar(1000),
    	@CreateSwitchOutTableCMD varchar(max),
    	@DropExistingTableCMD varchar(max)

DECLARE @tab TABLE (ID INT identity, PartitionID INT)

IF (select object_ID(@Table_Name)) is null
BEGIN
SET @Error = 'Error: Proc = '+ object_name(@@procid)+', Message = Table '+@Table_Name+' Not Found.'
RAISERROR(@Error ,10,1,'')with log
RETURN 
END

SELECT 
@Table_Name_SwitchOut = @Table_Name+'_SwitchOut', --All switch out Table have this format.
@truncate = 'truncate table dbo.'+@Table_Name_SwitchOut,-- Create dynamic truncate as must be run after each switch. Switch out table must be empty before switch.
@now = dbo.datetime2uts_id(getdate()), @DaysPartitionsToKeep = 14, ---Get Id for Current Date
@DropExistingTableCMD = 'if (select object_id('''+@Table_Name_SwitchOut+''')) is not null drop table '+ @Table_Name_SwitchOut

exec mnt_GetSQLForSwitchOutTable @table_name, @Table_Name_SwitchOut,@CreateSwitchOutTableCMD output
exec (@DropExistingTableCMD)

print @CreateSwitchOutTableCMD
exec (@CreateSwitchOutTableCMD)


IF (select object_ID(@Table_Name_SwitchOut)) is null
BEGIN
SET @Error = 'Error: Proc = '+ object_name(@@procid)+', Message = Table '+@Table_Name_SwitchOut+' Not Created.'
RAISERROR(@Error,10,1,'')with log
RETURN 
END

SELECT 
@PartionScheme = ps.[name],
@PartitionFunction = pf.[name]
FROM sys.partitions p
JOIN sys.indexes i
   ON p.[object_id] = i.[object_id]
   AND p.index_id = i.index_id
JOIN sys.data_spaces ds
   ON i.data_space_id = ds.data_space_id
JOIN sys.partition_schemes ps
 ON ds.data_space_id = ps.data_space_id
JOIN sys.partition_functions pf
   ON ps.function_id = pf.function_id
WHERE p.[object_id] = OBJECT_ID(@Table_Name)

SELECT @PartitionsToKeep = CASE @PartionScheme
    WHEN   'PS_daily' THEN @DaysPartitionsToKeep 
    WHEN 'PS_weekly' THEN @DaysPartitionsToKeep/7
    ELSE -1 END

IF @PartitionsToKeep = -1
BEGIN
SET @Error = 'Error: Proc = '+ object_name(@@procid)+', Message = Unknown Partition Scheme '+@PartionScheme +' Not Found.'
RAISERROR(@Error,10,1,'')with log
RETURN 
END

---A little check to ensure the partioning function ranges hasnt changed, for example ,from daily to 
--weekly. 

SELECT @PartionCheck = DATEDIFF(d,MIN(dbo.uts_id2datetime([value],1)), 
MAX(dbo.uts_id2datetime([value],1)))+1
FROM(
SELECT TOP (@PartitionsToKeep) CAST([value] as BIGINT)[value]
FROM  sys.partition_range_values  rv
JOIN sys.partition_functions f
    ON rv.function_id = f.function_id
WHERE name = @PartitionFunction
AND [value] < @now
)x


IF ISNULL(@PartionCheck,9999)  > @DaysPartitionsToKeep 
BEGIN
SET @Error = 'Error: Proc = '+ object_name(@@procid)+', Message = Partition function is inconsistent and a truncate would only keep '+
    													cast(ISNULL(@PartionCheck,0)as varchar(10))+' days data.'
RAISERROR(@Error ,10,1,'')
RETURN 
END

---Now we do the work
INSERT @tab
SELECT distinct Partition_Number -- partition numbers for delete
FROM   sys.partitions
WHERE  [OBJECT_ID]= OBJECT_ID(@Table_Name)
   AND Partition_Number + @PartitionsToKeep <=
       (SELECT MAX(Partition_Number) --Latest partition with rows.
       FROM    sys.partitions
       WHERE   [OBJECT_ID] = OBJECT_ID(@Table_Name)
       AND [Rows]    > 0
       )
and   [Rows]    > 0



SET @no   = SCOPE_IDENTITY()
SET @i    =1
WHILE @i <= @no -- Loop through partition number generating and excuting alter and truncate.
BEGIN

        SELECT @sql = 'ALTER TABLE '+@Table_Name+'  SWITCH PARTITION '+ CAST(PartitionID AS VARCHAR(10))+ ' TO '+@Table_Name_SwitchOut 
        FROM   @tab
        WHERE  ID = @i
    	EXEC (@sql) --Alter table switch
       EXEC (@truncate) -- Truncate switch table
    	print @sql
    	print @truncate
SET @i = @i + 1
END

exec('drop table '+@Table_Name_SwitchOut)
programming t-sql sql code blog
Posted by: Harry McCarney
Last revised: 02 Oct, 2011 01:14 PM History

Comments

Ronald de Groot
Ronald de Groot
20 Jan, 2012 08:34 PM

Hello Harry,

Wanted to run your (nice looking) code, seems that dbo.datetime2uts_id is missing, can you tell me what is for? or how the code look?

Met vriendelijke groet, Ronald

Howardleaton
Howardleaton
15 May, 2012 01:19 AM

hi there gaza this is there contact info , there there most competitive in the game ,say Howard give you there number

garydirtan
garydirtan
11 May, 2012 04:59 AM

hi linda here is there web address address ,ring them if you need them in a hurry ,just say gary d recommened you

Your Comments

Used for your gravatar. Not required. Will not be public.
Posting code? Indent it by four spaces to make it look nice. Learn more about Markdown.

Preview