Pick source and target, map the columns SSIS-style, tick the keys. The statement is regenerated on every click.
S.Name <> T.Name misses changes to and from NULLHOLDLOCK: parallel loads insert the same key twiceEXISTS (... EXCEPT ...), re-run touches 0 rowsWITH (HOLDLOCK) on by defaultTHROW 50001 before anything changesOUTPUT INTO #temp + INSERT, one transaction
1Source columns: tick what to use, views allowed as source
2Mapping: target column auto-mapped by name, both data types, change type per column
3Keys: preselected from the target PK, source PK if the surrogate key isn't mapped
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 DELETE -- optional
OUTPUT $action AS MergeAction, COALESCE(inserted.[CustomerNo], deleted.[CustomerNo]);
| Change type | Upsert / SCD 1 | SCD type 2 |
|---|---|---|
| Type 2: history | overwrite | Change closes the current version and inserts a new one |
| Type 1: overwrite | overwrite | Overwritten in all versions of the key, no new version |
| Fixed | insert only | Never updated, only set on the first insert |
IsCurrent, IstAktuell: 1 for the current version
ValidTo, GueltigBis, EndDate: open end NULL or '9999-12-31'
ValidFrom, GueltigAb, StartDate: optional
INSERT INTO dim SELECT ... FROM (MERGE ... OUTPUT ...) is rejected by SQL Server as soon as the dimension has a trigger or is referenced by a foreign key from a fact table. That is most real dimensions.
MERGE ... OUTPUT INTO #smgMergeOutput, then a plain INSERT for every closed key still in the source. XACT_ABORT ON, one transaction, triggers and FKs both covered by the live test.
| Option | Effect | Default |
|---|---|---|
| Update only on real change | WHEN MATCHED AND EXISTS (... EXCEPT ...) | on |
| Not in source | Upsert: DELETE · SCD 2: close the current version | off |
| WITH (HOLDLOCK) | No race between parallel loads | on |
| OUTPUT $action | Action and key per row (SCD 2: summary row) | off |
| Duplicate key check | THROW 50001 before anything changes | on |
| Source filter | WHERE inside the USING subquery | empty |
$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
| Function | Purpose |
|---|---|
| Show-sqmMergeGeneratorGui | The GUI, optionally connected right away |
| New-sqmMergeStatement | Pure text generator for pipelines and deployment scripts |
| Get-sqmMergeDatabase / -Table / -Column | Metadata via System.Data.SqlClient |
] escaped