powershelldba.de · Uwe Janke
REF: PSDB-PART-2026 SCOPE: SQL Server 2016–2022 STATUS: v1.16.0
Module

Automatic SQL Server table partitioning, sqmPartitionTool

Partition existing tables, maintain them automatically (sliding window + retention), and migrate them completely into a separate archive database, with a transparent cutover via a view, so existing application code keeps running unchanged. Data is moved with the resumable, chunked copy engine of sqmDataTransfer. Built on dbatools, sqmSQLTool and sqmDataTransfer.

PowerShell 5.1+ Built on dbatools SQL Server 2016–2022 dtcSoftware © Uwe Janke

What is sqmPartitionTool?

sqmPartitionTool is a PowerShell module by dtcSoftware (author: Uwe Janke) for the full lifecycle of a partitioned SQL Server table: from the one-time conversion of an existing table, through ongoing automatic maintenance (creating new partitions ahead of time, removing old ones after their retention period), to fully migrating an entire table into a separate archive database.

The module is built on dbatools and sqmSQLTool (reusing its logging, configuration, and WinForms theme) and supports numeric and string date-surrogate keys alongside real date columns, as well as composite keys with no single unique column, situations that come up regularly in production databases that have grown organically.

Data is moved between databases with the copy engine of sqmDataTransfer: SqlBulkCopy in chunks (one month or one target partition each), resumable after any stop without needing a key, with throughput per chunk on the console. See How data is copied.

PowerShell 5.1+ dbatools sqmSQLTool ≥ 1.9.2.0 sqmDataTransfer ≥ 0.1.23.0 SQL Server 2016–2022

Feature highlights

In-place partitioning

Partition an existing table directly in its source database, including a variant for very large tables with little free disk space.

Sliding-window maintenance

New, empty partitions are created automatically ahead of time, the last partition never fills up.

Automatic retention

Expired partitions are removed automatically, optionally copying them to an archive database first.

Archive-DB migration with cutover

Migrate an entire table month by month into an archive database, optionally including the current month, ending with an atomic switch to a view over the copy, transparent to existing application code.

Composite keys and date surrogates

Up to five columns as a key, plus numeric/string date-surrogate keys (e.g. an INT column formatted as YYYYMMDD).

Proven copy engine

Data moves through sqmDataTransfer's SqlBulkCopy engine: chunked, resumable without a key, with rows/s per chunk on the console.

Re-partition a copy

Copy an already partitioned table into another database with a different granularity or partition column, the source stays untouched.

GUI wizard + CLI

A WinForms wizard for the whole process, every core function also works standalone from the PowerShell console.

A, In-place partitioning

Invoke-sqmTablePartitionConversion , partition an existing table, even with little free disk space

What it does

Converts an existing, non-partitioned table into a partitioned one, pre-flight check, min/max detection, boundary calculation, filegroup creation, and the actual partitioning in one call. Works for heaps and clustered-index tables, including automatically extending the PRIMARY KEY with the partition column when needed. -Method BatchedSwap builds a new, empty partitioned copy for very large tables and moves the data in segments with periodic DBCC SHRINKFILE, instead of one single large operation. Add -ViewCutover and reads against the table stay complete and consistent for the whole (potentially hours-long) migration, instead of seeing a shrinking subset as rows move — a read-only bridge view takes the table's place within seconds of the migration starting. With -FilePath the new filegroup goes to a folder of your choice on the server, for example a dedicated drive (the folder is created if missing; the same parameter exists for the archive migration and the copy).

When to use it

A table that has grown large and isn't partitioned needs to be partitioned without downtime risk. -Method BatchedSwap specifically when the disk doesn't have enough free space for a classic single-step conversion.

Common pitfalls

  • A PRIMARY KEY/UNIQUE constraint without the partition column in the key, extending it changes uniqueness semantics and must be confirmed deliberately
  • -Method BatchedSwap currently doesn't support tables with incoming foreign keys or triggers
  • Without enough free disk space, the standard method fails partway through the operation
  • -ViewCutover is read-only by design — writes against the table name fail while the bridge view is in place, only use it where write traffic can tolerate that for the migration's duration

Benefits

  • Automatically detects the right approach (constraint extension, index rebuild, or table swap)
  • Segment-by-segment migration with shrink, for environments with little free disk space
  • Automatically registers the table for automatic maintenance (scenario B)
  • -ViewCutover keeps reads seeing the complete dataset throughout the whole migration, not just before and after it
# Partition an existing table by month
Invoke-sqmTablePartitionConversion -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -PartitionColumn "OrderDate" -Granularity Month -AllowKeyChange

# Very large table, little free disk space
Invoke-sqmTablePartitionConversion -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -PartitionColumn "OrderDate" -Granularity Month -Method BatchedSwap -DataCompression Page

# Same, but reads stay complete/consistent for the whole migration (read-only during the run)
Invoke-sqmTablePartitionConversion -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -PartitionColumn "OrderDate" -Granularity Month -Method BatchedSwap -ViewCutover

B, Automatic maintenance

New-sqmPartitionExtendJob , sliding-window extension as a SQL Agent job

What it does

Creates a SQL Agent job that periodically runs sqm_ExtendPartitionWindow (a T-SQL procedure), it reads the registry of all registered tables and creates new, empty partitions within the configured lead time (FutureBufferPeriods). Idempotent: running it multiple times on the same day changes nothing.

When to use it

Set this up once, right after the conversion (scenario A), after that, extension runs automatically forever, without anyone needing to remember before "the last partition" fills up.

Common pitfalls

  • Without this job, the most recently created partition eventually fills up and new rows land in the wrong, last partition
  • Needs rights on msdb to create the SQL Agent job

Benefits

  • Fully automatic, no manual intervention after initial setup
  • Covers every table registered in sqm_PartitionRegistry at once
# Set up the maintenance job for sliding-window extension
New-sqmPartitionExtendJob -SqlInstance "SQL01"

Register-sqmPartitionTable + New-sqmPartitionRetentionJob, automatic retention and archiving

What it does

Registers a table with a retention period (-RetentionValue/-RetentionUnit) and an optional target archive database (-ArchiveEnabled/-ArchiveDatabaseName). New-sqmPartitionRetentionJob then creates the matching SQL Agent job, which removes expired partitions via Invoke-sqmPartitionArchive, optionally only after copying them to the archive database first (SWITCH PARTITION, batch copy, MERGE RANGE).

When to use it

Old partitions should disappear automatically after a set period, either deleted permanently, or copied first to a cheaper or differently-backed-up archive database.

Common pitfalls

  • Without -ArchiveEnabled, expired partitions are deleted permanently, test before using in production
  • Only affects individual partitions of a still-active table as they expire one by one, not the whole table at once (for that, see scenario C)

Benefits

  • Configurable retention in months or years
  • Optional archiving instead of deletion, with no extra manual effort
# Set up retention with archiving instead of deletion
Register-sqmPartitionTable -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -RetentionValue 36 -RetentionUnit Months -ArchiveEnabled -ArchiveDatabaseName "SalesArchive"
New-sqmPartitionRetentionJob -SqlInstance "SQL01"

Invoke-sqmPartitionRetention , ad-hoc retention for a single table, right now

What it does

Removes every partition of one table that's older than a cutoff you choose at call time (e.g. 120 months / 10 years), repeatedly calling Invoke-sqmPartitionArchive under the hood (SWITCH PARTITION, optional archive copy, MERGE RANGE), oldest first, until nothing left is older than the cutoff. No registration required — it reads each partition's boundary value directly instead of relying on sqm_PartitionRegistry.

When to use it

Testing the retention behavior before enabling the scheduled job, an emergency "we need space now" cleanup, or a one-off table that was never registered for automatic maintenance.

Common pitfalls

  • Removes the partition itself (boundary gone, partition count shrinks) — not SQL Server's TRUNCATE PARTITION, which only empties the data and leaves the partition slot in place
  • Without -ArchiveDatabaseName, removed data is deleted permanently

Benefits

  • One confirmation for the whole run, not one per partition, and it shows the affected count up front
  • -WhatIf shows exactly how many partitions would be removed without changing anything
  • Works on unregistered tables, no prerequisite setup
# Remove everything older than 10 years from one table, right now
Invoke-sqmPartitionRetention -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -RetentionValue 120 -RetentionUnit Months

C, Archive-DB migration with cutover

Invoke-sqmTableArchiveMigration , move an entire table into an archive database, transparently via a view

What it does

Migrates an active, not yet partitioned table into a partitioned copy in a separate archive database, one calendar month per chunk. On the first call the archive table is created with the source's structure and partitioned by month over the source's actual date range; -PrimaryKeyFromUniqueIndex turns an existing unique nonclustered index of the source into the clustered PRIMARY KEY of the copy. Every month is then copied with the copy engine of sqmDataTransfer (SqlBulkCopy), with rows, duration and rows/s per month on the console.

Works with real date columns as well as date surrogates (an INT or string column holding YYYYMMDD or YYYYMM). By default the migration stops at the previous month. -IncludeOpenPeriods transfers everything, including the current month: open months are copied again on every run, so rows written into the still active source later, and changed rows, follow automatically. -PurgeSourceAfterArchive deletes each completed month from the source after a row-count check and reclaims the space. -CutoverToArchiveView finally renames the source table and replaces it with a view over the archive copy, existing application code keeps using the same table name.

When to use it

An entire, active table needs to move permanently to another database, e.g. one with a different backup or storage profile, without touching application code. Built for very large tables (hundreds of millions of rows): every month is its own chunk, a stopped run loses at most the month in progress, and the console shows throughput per month instead of staying silent for hours.

Common pitfalls

  • The archive database must already exist, it isn't created automatically
  • Without an index that has the date column as its leading column, every month reads the whole source table, create one first for very large tables
  • Without -IncludeOpenPeriods the current month stays in the source, and after a cutover in the renamed original table
  • Open months are never deleted by -PurgeSourceAfterArchive: rows arriving between the row count and the delete would be lost
  • A key is only needed for the cutover together with -IncludeOpenPeriods (final sync, see below). For a heap pass -PrimaryKeyFromUniqueIndex or -KeyColumn (up to five columns)
  • The renamed original table is never deleted automatically, deliberately, check it before dropping it for good

Benefits

  • Resumable without a key: each run counts rows per month in source and archive (one GROUP BY each). A month with equal counts is skipped, a month with a different count (leftover of a stopped run) is emptied in the archive and copied again, no duplicates
  • A month only counts as Completed in dbo.sqm_ArchiveMonthLog after its row count has been verified
  • Cutover with -IncludeOpenPeriods is atomic: final sync of the open months, rename and view creation run in one transaction under an exclusive table lock, no row can slip in between
  • Self-healing after earlier attempts: if the archive table was dropped, stale log entries are reset, and an unused partition function/scheme left over from an earlier run is recreated with the current boundaries instead of being reused
  • The archive copy is registered automatically for its own sliding-window maintenance

Step by step, without the GUI

The same function is called several times with the same base parameters; every call continues where the previous one stopped. Only step 5 is hard to undo.

# 0. Common parameters
Import-Module sqmPartitionTool
$p = @{
    SqlInstance               = 'SQL01'
    Database                  = 'Sales'
    Schema                    = 'dbo'
    Table                     = 'Bookings'
    ArchiveDatabaseName       = 'SalesArchive'
    DateColumn                = 'BOOKDATE'       # INT, YYYYMMDD
    PrimaryKeyFromUniqueIndex = 'UX_Bookings'    # becomes the PK of the archive copy
    IncludeOpenPeriods        = $true            # everything, including the current month
}

# 1. Check without changing anything
Test-sqmPartitionReadiness -SqlInstance 'SQL01' -Database 'Sales' -Schema 'dbo' -Table 'Bookings' -PartitionColumn 'BOOKDATE'
Invoke-sqmTableArchiveMigration @p -WhatIf

# 2. Optional: only create the partitioned archive table, inspect it
Invoke-sqmTableArchiveMigration @p -CreateArchiveTableOnly

# 3. Transfer the data, repeat as often as needed (resumes, re-syncs the open month)
Invoke-sqmTableArchiveMigration @p
#    in stages:            -StartPeriod 202401 -EndPeriod 202406
#    little disk space:    -PurgeSourceAfterArchive

# 4. Check
Get-sqmPartitionStatus -SqlInstance 'SQL01' -Database 'SalesArchive' -Schema 'dbo' -Table 'Bookings'

# 5. Cutover: final sync + rename + view in one transaction
Invoke-sqmTableArchiveMigration @p -CutoverToArchiveView

# 6. Create future month partitions automatically
New-sqmPartitionExtendJob -SqlInstance 'SQL01'

D, Copy an already partitioned table with new partitioning

Copy-sqmPartitionedTable , a second, independently partitioned copy in another database

What it does

Copies a table that is already partitioned into a new table in another database on the same instance, with a different partitioning: other granularity (month, quarter, year), other partition column or other filegroup strategy. The partition column is taken from the source unless given. The source stays completely unchanged and active, no rename, no view. The target table is created with the source's structure and indexes on the new partition scheme, then filled one chunk per partition of the new target with the copy engine of sqmDataTransfer. Without -TargetTableName the copy gets the source's name. With -CreateTableOnly only the empty, newly partitioned target table is created; a later call without the switch copies the data into it.

When to use it

A reporting or analysis copy with coarser partitions, or a test of a new partitioning before the production table is converted.

Common pitfalls

  • The target database must already exist, and source and target must be on the same instance
  • The source stays active: if rows arrive during the copy, the final row-count check reports the difference, another call copies it
  • -KeyColumn is ignored since 1.15.0, the copy needs no key
  • -FilePath is a folder on the SQL Server, not on your workstation; it only applies to newly created filegroups, an existing one is reused and not moved

Benefits

  • Resumable per partition: rows per target partition are counted in source and target via $PARTITION of the new partition function, complete partitions are skipped, a partial one is emptied and copied again
  • -MaxDurationMinutes stops cleanly between two partitions, for maintenance windows
  • Registered automatically for maintenance, unless -NoRegister
  • Create the target table first and inspect it, copy later: -CreateTableOnly
  • New filegroups on a drive of your choice with -FilePath
# Copy into a reporting database, partitioned by quarter instead of month
Copy-sqmPartitionedTable -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -TargetDatabaseName "SalesReporting" -Granularity Quarter

# Spread over several maintenance windows, at most 90 minutes each
Copy-sqmPartitionedTable -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -TargetDatabaseName "SalesReporting" -Granularity Year -MaxDurationMinutes 90

# Only create the partitioned target table, filegroup on drive G: of the server, copy later
Copy-sqmPartitionedTable -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -TargetDatabaseName "SalesReporting" -Granularity Year -FilePath 'G:\SQLData\Reporting' -CreateTableOnly

How data is copied

Since version 1.15.0 every copy between databases runs through the copy engine of sqmDataTransfer, the same path as its chunked transfer that is used in production for tables with hundreds of millions of rows. Before, sqmPartitionTool used its own T-SQL batches (MERGE and keyset INSERT ... SELECT TOP); without a matching index every batch read the whole table, and the console stayed silent for hours.

What the engine brings along: SqlBulkCopy with explicit, name-based column mapping (a computed column in the middle of the table cannot shift columns), the source query is cancelled on the server when a chunk fails instead of reading its rest over the network, batch size capped automatically for columnstore targets, and progress per chunk.

FunctionOne chunk isResume after a stop
Invoke-sqmTableArchiveMigrationone calendar monthrow count per month, source vs. archive
Copy-sqmPartitionedTableone partition of the new targetrow count per target partition via $PARTITION
Invoke-sqmTableRelocationautomatically detected chunk column (date, period or YYYYMMDD, by month), otherwise the whole tablerow count per chunk

Each chunk is read by index seek instead of a full scan of the source table when an index leads with the date or partition column (since 1.15.1). On a heap SQL Server otherwise reads the whole table for every month, and the archive's INSERT BULK session waits on ASYNC_NETWORK_IO while the scan passes other months.

In all three cases a chunk with equal counts on both sides is skipped and a chunk with different counts is emptied in the target and copied again. No primary or unique key is needed for that.

Deliberately unchanged: the retention archiving of single expired partitions (Invoke-sqmPartitionArchive, staging table to archive on the same instance, DELETE ... OUTPUT INTO, transactional per batch, runs unattended in the retention job) and the in-place conversion -Method NewTableSwap (INSERT ... WITH (TABLOCK) inside the same database). Routing these through a client would only add a network hop.

# Console output of an archive migration
[10:23:57] Counting rows per month in source and archive ...
[10:23:58] Archiving period 202601 (1 of 10): 620 row(s) ...
[10:23:59] Period 202601 done: 620 row(s) in 0.8 s (793 rows/s, running total: 620).
[10:23:59] Archiving period 202602 (2 of 10): 560 row(s) ...

Practical notes for very large tables

  • Run it on the SQL Server host itself. SqlBulkCopy reads through the client: started on a workstation, every row crosses the network twice.
  • ASYNC_NETWORK_IO on the reading session is normal as long as writing is slower than reading. The bottleneck shows on the writing session (INSERT BULK): WRITELOG or log growth (FULL recovery logs every row, pre-size the log), PAGEIOLATCH (storage), LCK_M_* (blocking).
  • No index maintenance on the source during a copy. The reading session holds a schema lock on the source table; an index rebuild, TRUNCATE or partition SPLIT waits, and everything behind it queues up.
  • Batch size: without -BatchSize the default of sqmDataTransfer applies (Get-sqmTransferConfig DefaultBatchSize, 500,000).

GUI wizard

Show-sqmPartitionToolGui , a WinForms wizard for the whole process

What it does

Walks step by step through connection, table selection, column selection, min/max preview, granularity, boundary preview, and execution. Step 6 adapts to the selected table:

  • Not partitioned yet: either partition it in place with automatic maintenance (scenarios A and B), or Migrate to archive database now (scenario C) with the choices create the archive table only or create and transfer, include the current month (pre-selected, transfers everything), delete archived months from the source and cutover to a view. For a heap with a suitable unique index the wizard offers to make it the clustered primary key. A key-column picker only appears when a key is needed and cannot be derived.
  • Already partitioned: copy mode (scenario D), target database and optional target name. No key selection, the copy is resumable per partition.

The window title shows the module version and the folder it was loaded from, so it is visible at a glance which installed copy is running.

When to use it

For a one-time, interactively guided conversion of a table, with a preview before every critical step. For very long-running migrations (scenario C on very large tables), a direct PowerShell call is a better fit instead, since you don't want to keep the GUI window open for hours.

Common pitfalls

  • WinForms needs the desktop CLR, still available under PowerShell 7 on Windows, but not on PowerShell 7 on Linux/macOS

Benefits

  • Supports both Windows and SQL Server authentication
  • Boundary preview before actual execution, no surprises
  • Live log directly in the window during execution
# Open the GUI wizard
Show-sqmPartitionToolGui -SqlInstance "SQL01"
sqmPartitionTool partitioning wizard, step 6/8 boundary preview with 15 boundary values and 16 partitions, three of them marked as future buffer
Step 6/8 in the partitioning wizard: boundary preview before the actual partitioning, here, 16 monthly partitions, three already created ahead of time as a future buffer.

Quick start

Installation & first steps

# Install the dependencies first, then the module (as administrator, from the repository folders)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\sqmSQLTool\Install.ps1      -Scope AllUsers
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\sqmDataTransfer\Install.ps1 -Scope AllUsers
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\sqmPartitionTool\Install.ps1 -Scope AllUsers

# Import the module
Import-Module sqmPartitionTool

# GUI wizard
Show-sqmPartitionToolGui -SqlInstance "SQL01"

# Or directly via CLI: partition an existing table
Invoke-sqmTablePartitionConversion -SqlInstance "SQL01" -Database "Sales" -Schema "dbo" -Table "OrderHistory" -PartitionColumn "OrderDate" -Granularity Month

The installer checks that sqmSQLTool (≥ 1.9.2.0) and sqmDataTransfer (≥ 0.1.23.0) are present in the same scope and recent enough, and says which one is missing. Neither is on the PowerShell Gallery; dbatools is installed from the Gallery if missing.