powershelldba.de · Uwe Janke
REF: PSDB-SMG-2026 SCOPE: SQL Server 2012+ / T-SQL MERGE STATUS: v0.1.0.0
Module

Stop hand-writing MERGE. Map the columns, tick the keys, copy the statement.

sqmMergeGenerator is a PowerShell module with a WinForms GUI that writes T-SQL MERGE statements for you. Pick a server, a source and a target table, map the columns the way the SSIS column mapping editor does, tick the join keys, and the statement is regenerated on every click. It produces a NULL-safe upsert (SCD type 1) or a full slowly changing dimension type 2 load with a change type per column. It has no dbatools or other module dependency and never runs anything unless you press Execute and confirm.

Why this exists

A correct MERGE is forty lines of things to get wrong.

The column list appears in four clauses that must stay in sync. S.Name <> T.Name silently misses changes to and from NULL. Without HOLDLOCK two parallel loads insert the same key twice. A duplicate key in the source either fails with error 8672 or, in a type 2 dimension, quietly writes wrong history. sqmMergeGenerator reads the metadata of both tables and writes all of it.

By handWith sqmMergeGenerator
Column lists typed four times, one forgotten column per releaseColumns auto-mapped by name, every clause generated from the same mapping
WHEN MATCHED THEN UPDATE rewrites every row on every runEXISTS (SELECT S.. EXCEPT SELECT T..): NULL-safe, a re-run with unchanged data touches 0 rows
Duplicate source keys found by error 8672 at 3 a.m.THROW 50001 with a clear message before anything changes
SCD type 2 copied from a blog post that breaks on triggers and foreign keysOUTPUT INTO #temp plus a plain INSERT, one transaction, works with triggers and FK-referenced dimensions
xml or geography column breaks the comparisonNon-comparable types cast automatically for change detection
Identity column mapped, statement failsSET IDENTITY_INSERT wrapped automatically, column left out of UPDATE SET
Requirements

What it needs.

PowerShellWindows PowerShell 5.1 or PowerShell 7 on Windows (the GUI is WinForms), both verified
ModulesNone. Talks to SQL Server through System.Data.SqlClient from the .NET Framework, so it runs on locked-down servers without PSGallery access
SQL Server2012 or later (MERGE, THROW), tested against SQL Server 2022
Rights to generateRead access to the catalog views of the databases involved
Rights to executeSELECT on the source, INSERT/UPDATE/DELETE on the target
The GUI

Three columns side by side, the statement right below.

Show-sqmMergeGeneratorGui connects with Windows or SQL authentication, then offers database and table per combo box for both sides. Source and target may sit in different databases on the same instance; every name is generated three-part.

  1. Source columnsEvery column with its data type and a [PK] marker. Tick what to use. Views are offered as a source.
  2. Mapping source → targetTarget column per source column, auto-mapped by name (case-insensitive), both data types, and the change type per column. Computed, rowversion and GENERATED ALWAYS columns are never offered as a target.
  3. Key columns (join)Preselected from the target primary key. If that is a surrogate key that isn't mapped, the source primary key is used instead, the typical type 2 dimension case.
  4. Options and statementEvery change regenerates the statement. Copy it, save it as .sql, check its syntax (compiled with SET NOEXEC ON, names resolved, nothing executed) or execute it after a confirmation.
sqmMergeGenerator window connected to a lab SQL Server: source table dbo.Customer in database smgTestSrc, target table dim.Customer in smgTestTgt. The three columns show the ticked source columns, the mapping grid with source column, type, target column, type and change type, and CustomerNo ticked as the join key. The options row has SCD type 2 selected with ValidFrom, ValidTo and IsCurrent detected automatically, and the generated statement starts below with the header comment and the duplicate key check.
A type 2 dimension: the target has ValidFrom, ValidTo and IsCurrent, so the GUI switched to SCD type 2 on its own and preselected those columns. The xml column is left unmapped.
Mode 1: Upsert / SCD type 1

The target becomes a copy of the source, and a re-run changes nothing.

MERGE INTO [DWH].[dbo].[Customer] WITH (HOLDLOCK) AS T
USING (
    SELECT [CustomerNo], [Name], [City]
    FROM [Staging].[dbo].[Customer]
) AS S
    ON T.[CustomerNo] = S.[CustomerNo]
WHEN MATCHED AND EXISTS (SELECT S.[Name], S.[City]
        EXCEPT
        SELECT T.[Name], T.[City]) THEN          -- NULL-safe: only real changes
    UPDATE SET T.[Name] = S.[Name], T.[City] = S.[City]
WHEN NOT MATCHED BY TARGET THEN
    INSERT ([CustomerNo], [Name], [City])
    VALUES (S.[CustomerNo], S.[Name], S.[City])
WHEN NOT MATCHED BY SOURCE THEN                         -- optional
    DELETE
OUTPUT $action AS MergeAction, COALESCE(inserted.[CustomerNo], deleted.[CustomerNo]) AS [CustomerNo];

Unchanged rows are not touched at all: no log volume, no trigger work, no new rowversion. Columns marked Fixed are only written on insert.

Mode 2: Slowly changing dimension type 2

History where it matters, overwrite where it doesn't, per column.

A target with a current flag and/or a valid-to column (several German and English naming patterns are recognised) switches the GUI into type 2 mode. The key is the business key, the surrogate key is assigned by the table.

Change typeUpsert / SCD 1SCD type 2
Type 2: historyoverwriteA change closes the current version and inserts a new one
Type 1: overwriteoverwriteOverwritten in all versions of the key, no new version
Fixedinsert onlyNever updated, only set on the first insert

What the generated statement does, in one transaction

  1. Duplicate key checkTHROW 50001 if the source delivers a business key twice, before anything changes.
  2. Type 1 columnsOne UPDATE across every version of the key, only where the value really differs.
  3. MERGE against the current versionNew keys are inserted, a type 2 change closes the current version, optionally keys missing from the source are closed too. The affected rows go to #smgMergeOutput via OUTPUT INTO.
  4. New versionsA plain INSERT from the temp table for every closed key that is still in the source.
  5. SummaryOne row: new keys, new versions, closed keys, type 1 updates.

Why not the textbook pattern?

The well-known INSERT INTO dim SELECT ... FROM (MERGE ... OUTPUT ...) is composable DML, and SQL Server rejects it as soon as the dimension has a trigger or is referenced by a foreign key from a fact table. That is most real dimensions. sqmMergeGenerator uses OUTPUT INTO a temp table followed by an ordinary INSERT, so both cases work. Both are part of the live test.

Options

Every switch that matters, with sane defaults.

OptionUpsert / SCD 1SCD type 2
Update only on real changeWHEN MATCHED AND EXISTS (... EXCEPT ...)always
Not in sourceWHEN NOT MATCHED BY SOURCE THEN DELETEclose the current version
WITH (HOLDLOCK)yes, default onyes, default on
OUTPUT $actionkey and action per rowsummary row instead
Duplicate key checkTHROW 50001 before the MERGEsame
Source filterWHERE inside the USING subquerysame
Open endn/aNULL or a fixed date such as '9999-12-31'

xml, text, ntext, image and CLR types (geography, geometry, hierarchyid) can't be compared by EXCEPT directly; they are cast for the comparison and assigned unchanged.

Scripting without the GUI

A pure text generator for pipelines and deployment scripts.

New-sqmMergeStatement needs no database connection. Feed it the mapping and keys, get the statement back as a string.

FunctionPurpose
Show-sqmMergeGeneratorGuiThe WinForms GUI, optionally connected right away with -SqlInstance / -SqlCredential
New-sqmMergeStatementGenerates the upsert or SCD type 2 statement from source, target, mapping and keys, no connection needed
Get-sqmMergeDatabaseDatabases of an instance the login can read
Get-sqmMergeTableTables, optionally views, of a database
Get-sqmMergeColumnColumns with type, PK, identity, computed, rowversion, GENERATED ALWAYS, CLR flag and IsWritable
Quick start

Install, then the GUI, or one call.

# Installation - AllUsers when elevated, otherwise CurrentUser
Install.cmd

# GUI (or double-click Start-sqmMergeGenerator.cmd without installing)
Show-sqmMergeGeneratorGui -SqlInstance SQL01

# Upsert from a hashtable mapping source = target
New-sqmMergeStatement -SourceDatabase Staging -SourceTable Customer -TargetDatabase DWH -TargetTable Customer `
    -ColumnMapping @{ CustomerNo = 'CustomerNo'; Name = 'Name'; City = 'City' } -KeyColumns CustomerNo -HoldLock

# SCD type 2 with a change type per column
$map = @(
    @{ SourceColumn = 'CustomerNo'; TargetColumn = 'CustomerNo' }
    @{ SourceColumn = 'Name';       TargetColumn = 'Name';    ChangeType = 'Type1' }
    @{ SourceColumn = 'City';       TargetColumn = 'City';    ChangeType = 'Type2' }
    @{ SourceColumn = 'Segment';    TargetColumn = 'Segment'; ChangeType = 'Fixed' }
)
New-sqmMergeStatement -SourceDatabase Staging -SourceTable Customer -TargetDatabase DWH -TargetSchema dim -TargetTable Customer `
    -ColumnMapping $map -KeyColumns CustomerNo -Mode Scd2 -DuplicateKeyCheck -HoldLock `
    -ValidFromColumn ValidFrom -ValidToColumn ValidTo -CurrentFlagColumn IsCurrent

The last servers and the login name are remembered in %APPDATA%\sqmMergeGenerator\config.json; the password never is. The included live test (Tests\Test-sqmMergeGenerator.ps1, 36 checks) creates two throwaway databases, runs the generated statements against them and drops them again.

Next steps

Generate your first MERGE in under a minute.

No dependencies to install: unzip, double-click Start-sqmMergeGenerator.cmd, connect.