SQL Server Archive Process Using a Stored Procedure and SQL Agent Job

SQL Server Archive Process Using a Stored Procedure and SQL Agent Job – SQLYARD

SQL Server Archive Process Using a Stored Procedure and SQL Agent Job


Compatibility: This approach works on all SQL Server editions including Standard, from SQL Server 2008 through 2025. No Enterprise Edition required. See the companion article SQL Server High-Performance Archive Using Partition Switching and Filegroups for the Enterprise Edition approach that handles billions of rows.

When to Use This Approach

This stored procedure pattern is the right choice when you need a straightforward, maintainable archive process that works on any SQL Server edition. It is particularly well suited for:

  • Tables up to a few hundred million rows where archiving in batches is acceptable
  • Environments running SQL Server Standard Edition
  • Teams that prefer simple, readable T-SQL over partition management complexity
  • Situations where some archiving lag (minutes to hours) is acceptable
FactorThis ApproachPartition SWITCH Approach
Edition requiredAny editionEnterprise only
Setup complexityLowHigh
Archive speedMinutes to hoursMilliseconds
Log growthModerate (batched)Minimal
Blocking riskLow (with batching)Minimal
Best forSmaller tables, simple setupVery large tables, zero downtime

Process Overview

1
Filegroup Separate storage for archive data
2
Archive Table Same schema on archive filegroup
3
Log Table Audit trail of each archive run
4
Stored Proc Batched INSERT → DELETE with TRY/CATCH
5
Agent Job Scheduled automatic execution

Prerequisites and Planning

  • Identify the archive column — typically a date column like CreatedDate or OrderDate
  • Define the archive threshold — how old must data be before it is archived (e.g. 6 months, 1 year)
  • Confirm the archive table needs — same schema, same indexes, or a subset?
  • Determine batch size — how many rows per iteration to control log growth and blocking
  • Ensure disk space is available for the new filegroup file
  • SQL Server Agent must be running for automated scheduling

Before archiving production data: always test the stored procedure with @ExecuteArchive = 0 (the dry-run mode built into the procedure below) to confirm row counts and date boundaries before any data moves.

The Complete Implementation

1

Create the Archive Filegroup and File

Placing archive data on a dedicated filegroup separates it physically from production data. This gives you independent backup, restore, and storage management for archived data.

-- Add a dedicated archive filegroup to the database
ALTER DATABASE YourDatabase
ADD FILEGROUP ArchiveFG;

-- Add a data file to the filegroup
-- Update the FILENAME path to match your environment
ALTER DATABASE YourDatabase
ADD FILE
(
    NAME       = N'ArchiveFile',
    FILENAME   = N'D:\SQLData\ArchiveFile.ndf',
    SIZE       = 500MB,        -- Size based on expected archive volume
    MAXSIZE    = UNLIMITED,
    FILEGROWTH = 256MB
) TO FILEGROUP ArchiveFG;

-- Verify the filegroup was created
SELECT name, type_desc, is_default, is_read_only
FROM sys.filegroups
ORDER BY name;
2

Create the Archive Table

The archive table must live on the ArchiveFG filegroup. Match the schema of your source table exactly. Add an ArchivedDate column to record when each row was archived — useful for auditing and troubleshooting.

-- Create the archive table on the archive filegroup
-- Replace columns to match your actual table structure
CREATE TABLE dbo.YourTableArchive
(
    -- Match all columns from YourTable exactly
    ID            INT           NOT NULL,
    DateColumn    DATETIME      NOT NULL,
    Description   NVARCHAR(500) NULL,
    Amount        DECIMAL(18,2) NULL,
    CreatedBy     NVARCHAR(100) NULL,
    -- Add all your source table columns here

    -- Additional archive metadata column
    ArchivedDate  DATETIME      NOT NULL
        CONSTRAINT DF_YourTableArchive_ArchivedDate DEFAULT (GETDATE()),

    CONSTRAINT PK_YourTableArchive PRIMARY KEY CLUSTERED (ID)
)
ON ArchiveFG;  -- Placed on the archive filegroup

-- Add any nonclustered indexes matching the source table
-- that are needed for reporting queries against archive data
CREATE NONCLUSTERED INDEX IX_YourTableArchive_DateColumn
ON dbo.YourTableArchive (DateColumn)
ON ArchiveFG;
3

Create the Archive Log Table

The log table records every archive run — when it ran, how many rows moved, whether it succeeded, and any error messages. This is essential for monitoring and troubleshooting.

-- Archive process audit log
CREATE TABLE dbo.ArchiveProcessLog
(
    LogID            INT           IDENTITY(1,1) NOT NULL,
    RunDate          DATETIME      NOT NULL DEFAULT (GETDATE()),
    ArchiveThreshold DATETIME      NOT NULL,
    RowsArchived     INT           NOT NULL DEFAULT (0),
    RowsDeleted      INT           NOT NULL DEFAULT (0),
    BatchCount       INT           NOT NULL DEFAULT (0),
    DurationSeconds  INT           NULL,
    Status           VARCHAR(20)   NOT NULL DEFAULT ('Running'),
    ErrorMessage     NVARCHAR(MAX) NULL,
    CONSTRAINT PK_ArchiveProcessLog PRIMARY KEY CLUSTERED (LogID)
);
4

Create the Archive Stored Procedure

This is the core of the archive process. Key design decisions in this procedure:

  • Batched processing — moves rows in small batches to control log growth and reduce blocking
  • Transaction per batch — each batch commits independently so a failure does not roll back hours of work
  • TRY/CATCH — errors are caught, logged, and reported rather than failing silently
  • Dry-run mode — run with @ExecuteArchive = 0 to preview what would be archived
  • Configurable threshold — the archive cutoff date is a parameter, not hardcoded
CREATE PROCEDURE dbo.MoveDataToArchive
(
     @MonthsToRetain   INT  = 6      -- Keep this many months in main table
    ,@BatchSize        INT  = 10000  -- Rows per batch — adjust for your environment
    ,@ExecuteArchive   BIT  = 1      -- 0 = dry run (count only), 1 = execute
)
AS
/*
    MoveDataToArchive
    -----------------
    Moves data older than @MonthsToRetain months from YourTable
    to YourTableArchive in batches.

    Parameters:
        @MonthsToRetain  - Number of months of data to keep in the main table (default: 6)
        @BatchSize       - Rows moved per iteration (default: 10,000)
        @ExecuteArchive  - 0 = dry run shows row count only, 1 = execute the archive

    Test (dry run — no data moved):
        EXEC dbo.MoveDataToArchive @MonthsToRetain = 6, @ExecuteArchive = 0

    Test (execute with small batch for validation):
        EXEC dbo.MoveDataToArchive @MonthsToRetain = 6, @BatchSize = 1000, @ExecuteArchive = 1
*/
SET NOCOUNT ON;

DECLARE
     @ArchiveThreshold DATETIME
    ,@RowsThisBatch    INT
    ,@TotalArchived    INT = 0
    ,@TotalDeleted     INT = 0
    ,@BatchCount       INT = 0
    ,@LogID            INT
    ,@StartTime        DATETIME = GETDATE()
    ,@ErrorMessage     NVARCHAR(MAX);

-- Calculate the archive threshold date
SET @ArchiveThreshold = DATEADD(MONTH, -@MonthsToRetain, GETDATE());

-- -------------------------------------------------------
-- Dry run: show what would be archived without moving data
-- -------------------------------------------------------
IF @ExecuteArchive = 0
BEGIN
    SELECT
        'DRY RUN — No data moved'                     AS Mode,
        @ArchiveThreshold                             AS ArchiveThreshold,
        COUNT(*)                                      AS RowsToArchive,
        MIN(DateColumn)                               AS OldestRow,
        MAX(DateColumn)                               AS NewestRowToArchive
    FROM dbo.YourTable
    WHERE DateColumn < @ArchiveThreshold;
    RETURN;
END

-- -------------------------------------------------------
-- Log the start of this archive run
-- -------------------------------------------------------
INSERT INTO dbo.ArchiveProcessLog (RunDate, ArchiveThreshold, Status)
VALUES (GETDATE(), @ArchiveThreshold, 'Running');

SET @LogID = SCOPE_IDENTITY();

-- -------------------------------------------------------
-- Batched archive: INSERT then DELETE in small transactions
-- -------------------------------------------------------
BEGIN TRY

    WHILE 1 = 1
    BEGIN

        -- Insert one batch into the archive table
        INSERT INTO dbo.YourTableArchive
        (
            ID, DateColumn, Description, Amount, CreatedBy,
            -- list all columns from YourTable
            ArchivedDate
        )
        SELECT TOP (@BatchSize)
            ID, DateColumn, Description, Amount, CreatedBy,
            -- list all columns from YourTable
            GETDATE() AS ArchivedDate
        FROM dbo.YourTable
        WHERE DateColumn < @ArchiveThreshold
        ORDER BY DateColumn ASC;  -- Archive oldest first

        SET @RowsThisBatch = @@ROWCOUNT;

        -- No rows inserted — all qualifying data has been archived
        IF @RowsThisBatch = 0 BREAK;

        SET @TotalArchived += @RowsThisBatch;
        SET @BatchCount    += 1;

        -- Delete the same rows from the source table
        -- Match on the same criteria to avoid deleting wrong rows
        DELETE TOP (@BatchSize) FROM dbo.YourTable
        WHERE DateColumn < @ArchiveThreshold
        AND ID IN
        (
            SELECT TOP (@BatchSize) ID
            FROM dbo.YourTable
            WHERE DateColumn < @ArchiveThreshold
            ORDER BY DateColumn ASC
        );

        SET @TotalDeleted += @@ROWCOUNT;

        -- Brief pause between batches to reduce I/O pressure
        -- Remove or adjust based on your environment
        WAITFOR DELAY '00:00:01';

    END; -- end of WHILE loop

    -- Update statistics on both tables after archiving
    UPDATE STATISTICS dbo.YourTable;
    UPDATE STATISTICS dbo.YourTableArchive;

    -- Log successful completion
    UPDATE dbo.ArchiveProcessLog
    SET
        RowsArchived    = @TotalArchived,
        RowsDeleted     = @TotalDeleted,
        BatchCount      = @BatchCount,
        DurationSeconds = DATEDIFF(SECOND, @StartTime, GETDATE()),
        Status          = 'Completed'
    WHERE LogID = @LogID;

    -- Return summary
    SELECT
        @ArchiveThreshold  AS ArchiveThreshold,
        @TotalArchived     AS TotalRowsArchived,
        @TotalDeleted      AS TotalRowsDeleted,
        @BatchCount        AS BatchesProcessed,
        DATEDIFF(SECOND, @StartTime, GETDATE()) AS DurationSeconds;

END TRY
BEGIN CATCH

    SET @ErrorMessage = ERROR_MESSAGE();

    -- Log the failure
    UPDATE dbo.ArchiveProcessLog
    SET
        RowsArchived    = @TotalArchived,
        RowsDeleted     = @TotalDeleted,
        BatchCount      = @BatchCount,
        DurationSeconds = DATEDIFF(SECOND, @StartTime, GETDATE()),
        Status          = 'Failed',
        ErrorMessage    = @ErrorMessage
    WHERE LogID = @LogID;

    -- Re-raise the error so the Agent job captures it
    RAISERROR(@ErrorMessage, 16, 1);

END CATCH;
GO

Why batching matters: A single DELETE of 50 million rows generates a transaction log entry for every deleted row, can grow the log file by gigabytes, and holds locks for the entire duration. Batching at 10,000 rows commits frequently, keeps log growth small and predictable, and allows other sessions to access the table between batches.

5

Test the Stored Procedure

Always test before scheduling. Use the dry-run mode first to confirm what data qualifies, then run with a small batch size to validate the archive and delete logic before full execution.

-- Step 1: Dry run — confirms row count and date range, moves nothing
EXEC dbo.MoveDataToArchive
    @MonthsToRetain = 6,
    @ExecuteArchive = 0;

-- Step 2: Execute with small batch to validate first 1,000 rows
EXEC dbo.MoveDataToArchive
    @MonthsToRetain = 6,
    @BatchSize      = 1000,
    @ExecuteArchive = 1;

-- Step 3: Confirm rows moved correctly
SELECT COUNT(*) AS MainTableCount  FROM dbo.YourTable        WHERE DateColumn < DATEADD(MONTH, -6, GETDATE());
SELECT COUNT(*) AS ArchiveCount    FROM dbo.YourTableArchive WHERE DateColumn < DATEADD(MONTH, -6, GETDATE());

-- Step 4: Check the log
SELECT * FROM dbo.ArchiveProcessLog ORDER BY LogID DESC;
6

Schedule with SQL Server Agent

Once validated, create a SQL Server Agent job to run the archive procedure automatically on a schedule.

USE msdb;
GO

-- Create the Agent job
EXEC sp_add_job
    @job_name = N'DBA - Archive YourTable Data';

-- Add the job step
EXEC sp_add_jobstep
    @job_name      = N'DBA - Archive YourTable Data',
    @step_name     = N'Run Archive Procedure',
    @database_name = N'YourDatabase',
    @command       = N'EXEC dbo.MoveDataToArchive
                           @MonthsToRetain = 6,
                           @BatchSize      = 10000,
                           @ExecuteArchive = 1;',
    @on_success_action = 1,  -- Quit with success
    @on_fail_action    = 2;  -- Quit with failure (alerts DBA via Agent notification)

-- Schedule: run weekly on Sunday at 2:00 AM
EXEC sp_add_schedule
    @schedule_name     = N'Weekly Sunday 2AM',
    @freq_type         = 8,      -- Weekly
    @freq_interval     = 1,      -- Sunday
    @freq_recurrence_factor = 1,
    @active_start_time = 020000; -- 02:00:00

EXEC sp_attach_schedule
    @job_name      = N'DBA - Archive YourTable Data',
    @schedule_name = N'Weekly Sunday 2AM';

EXEC sp_add_jobserver
    @job_name = N'DBA - Archive YourTable Data';
GO
Data VolumeRecommended ScheduleBatch Size
Low — up to 1M rows/monthWeekly10,000–25,000
Medium — 1M–10M rows/monthNightly25,000–50,000
High — 10M+ rows/monthNightly or consider partition switching50,000–100,000

Monitoring and Maintenance

Checking Archive History

-- Archive run history — last 30 days
SELECT
    LogID,
    RunDate,
    CONVERT(VARCHAR, ArchiveThreshold, 120) AS ArchiveThreshold,
    RowsArchived,
    BatchCount,
    DurationSeconds,
    Status,
    ErrorMessage
FROM dbo.ArchiveProcessLog
WHERE RunDate >= DATEADD(DAY, -30, GETDATE())
ORDER BY LogID DESC;

-- Archive table growth over time
SELECT
    CAST(ArchivedDate AS DATE)  AS ArchiveDate,
    COUNT(*)                    AS RowsArchivedThatDay
FROM dbo.YourTableArchive
GROUP BY CAST(ArchivedDate AS DATE)
ORDER BY ArchiveDate DESC;

Making the Archive Filegroup Read-Only

Once you are confident no further writes are needed to the archive (for example, once a year's archiving is complete and verified), mark the filegroup read-only. This protects the data and simplifies backup strategy:

-- Make the archive filegroup read-only
ALTER DATABASE YourDatabase
MODIFY FILEGROUP ArchiveFG READ_ONLY;

-- Revert if needed
ALTER DATABASE YourDatabase
MODIFY FILEGROUP ArchiveFG READ_WRITE;

Backup Considerations

The archive filegroup is part of your database and included in full database backups automatically. Once the filegroup is marked read-only, you only need to back it up once — it never changes. This can significantly reduce backup time for databases with large archive filegroups.

Pre-Production Checklist

  • Dry run completed and row counts verified
  • Archive table schema matches source table exactly
  • Batch size tested and tuned for your environment's log and I/O capacity
  • Archive log table exists and receiving entries correctly
  • SQL Server Agent job tested in development before production deployment
  • Job failure notification configured on the Agent operator
  • Disk space on the archive filegroup confirmed sufficient
  • Backup jobs tested and confirmed to include the archive filegroup
  • Permissions confirmed — executing login has INSERT on archive table and DELETE on source
  • Archive threshold documented and agreed with business stakeholders

Summary

This stored procedure pattern gives you a safe, auditable, and fully automated archive process that works on every SQL Server edition. The key improvements over a naive INSERT/DELETE approach are batched processing to control log growth, transaction-per-batch to limit rollback scope, TRY/CATCH for error visibility, a dry-run mode for safe validation, and a complete audit log of every archive run.

Start with the dry-run mode, validate the row counts, then deploy with a conservative batch size. You can always increase the batch size once you have observed the impact on your specific environment. Small and safe beats fast and risky every time.

References


Discover more from SQLYARD

Subscribe to get the latest posts sent to your email.

Leave a Reply

Discover more from SQLYARD

Subscribe now to keep reading and get access to the full archive.

Continue reading