Lab: Every SQL Migration Scenario β Hands-On
Execute every major migration path to Azure SQL: backup/restore to VM, BACPAC to SQL DB, DMS online to SQL DB, Managed Instance Link, Log Replay Service, and transactional replication. Compare downtime, complexity, and when to use each.
DP-300 Domain: 1 β Plan and Implement Data Platform Resources (20-25%)
Customers always ask: "What's the best way to migrate?" The answer is always "it depends." This lab lets you execute every method and build real experience choosing the right one for each scenario.
Migration Methods Overviewβ
Architectureβ
Decision Matrixβ
| Method | Target | Downtime | Complexity | Max DB Size | Cost |
|---|---|---|---|---|---|
| Backup β Restore | SQL VM | Hours | β Low | Unlimited | $ |
| BACPAC | SQL DB | Hours | β Low | ~200 GB practical | $ |
| DMS Online | SQL DB | Minutes | ββ Medium | 4 TB | $$ |
| MI Link | SQL MI | Seconds | ββ Medium | 16 TB | $$$ |
| Log Replay (LRS) | SQL MI | Minutes | ββ Medium | 16 TB | $ |
| Transactional Replication | SQL DB | Seconds | βββ High | 4 TB | $$ |
Prerequisitesβ
- Azure subscription
- Azure CLI installed
- PowerShell 5.1+
- SqlPackage installed (for BACPAC)
- Azure Database Migration Service (for DMS scenario)
Step 0: Deploy Shared Lab Environmentβ
All scenarios use the same source VM. Deploy once, reuse for all:
$resourceGroup = "rg-migration-lab"
$location = "westeurope"
$suffix = Get-Random -Maximum 9999
$adminUser = "sqladmin"
$adminPassword = "<YOUR_STRONG_PASSWORD>"
# Create resource group
az group create --name $resourceGroup --location $location
# Source VM β on-prem simulation
az vm create `
--resource-group $resourceGroup `
--name sqlvm-source `
--image MicrosoftSQLServer:sql2022-ws2022:sqldev-gen2:latest `
--size Standard_D2ads_v6 `
--admin-username azureadmin `
--admin-password $adminPassword `
--location $location `
--public-ip-sku Standard
# Wait for VM, then install AdventureWorks
# RDP into sqlvm-source and run:
# On sqlvm-source β download and restore AdventureWorks
$bakUrl = "https://github.com/Microsoft/sql-server-samples/releases/download/adventureworks/AdventureWorks2022.bak"
$bakPath = "C:\Temp\AdventureWorks2022.bak"
New-Item -Path "C:\Temp" -ItemType Directory -Force
Invoke-WebRequest -Uri $bakUrl -OutFile $bakPath
# Restore
Invoke-Sqlcmd -Query @"
RESTORE DATABASE [AdventureWorks2022]
FROM DISK = '$bakPath'
WITH MOVE 'AdventureWorks2022' TO 'F:\SQLData\AdventureWorks2022.mdf',
MOVE 'AdventureWorks2022_log' TO 'F:\SQLLog\AdventureWorks2022_log.ldf',
REPLACE;
"@ -ServerInstance "localhost"
# Set FULL recovery model (required for DMS online, MI Link, LRS)
Invoke-Sqlcmd -Query "ALTER DATABASE [AdventureWorks2022] SET RECOVERY FULL;" -ServerInstance "localhost"
# Take a full backup (required baseline for LRS)
Invoke-Sqlcmd -Query @"
BACKUP DATABASE [AdventureWorks2022]
TO DISK = 'C:\Temp\AdventureWorks2022_full.bak'
WITH COMPRESSION, INIT;
"@ -ServerInstance "localhost"
Scenario 1: Backup β Blob β Restore (to Azure SQL VM)β
The simplest migration. Your existing SQL Migration Lab covers this in detail. Here's the summary:
# Create target VM
az vm create `
--resource-group $resourceGroup --name sqlvm-target `
--image MicrosoftSQLServer:sql2022-ws2022:sqldev-gen2:latest `
--size Standard_D2ads_v6 `
--admin-username azureadmin --admin-password $adminPassword `
--location $location --public-ip-sku Standard
# Create storage account for transfer
$storageAcct = "stmigrate$suffix"
az storage account create -g $resourceGroup -n $storageAcct -l $location --sku Standard_LRS
az storage container create --account-name $storageAcct --name backups
-- On source: backup to local disk
BACKUP DATABASE [AdventureWorks2022]
TO DISK = 'C:\Temp\AW2022.bak'
WITH COMPRESSION, INIT;
-- Upload via azcopy, download on target, then:
-- On target:
RESTORE DATABASE [AdventureWorks2022]
FROM DISK = 'F:\Temp\AW2022.bak'
WITH MOVE 'AdventureWorks2022' TO 'F:\SQLData\AdventureWorks2022.mdf',
MOVE 'AdventureWorks2022_log' TO 'F:\SQLLog\AdventureWorks2022_log.ldf',
REPLACE;
| Metric | Value |
|---|---|
| Downtime | Full backup + transfer + restore time (hours for large DBs) |
| RPO | Zero (consistent backup) |
| Complexity | Low β just backup and restore |
| When to use | Small-medium DBs, acceptable downtime, SQL VM target |
Scenario 2: BACPAC Export β Import (to Azure SQL DB)β
# Export BACPAC from source (run on sqlvm-source or any machine with SqlPackage)
SqlPackage /Action:Export `
/SourceServerName:"sqlvm-source-ip,1433" `
/SourceDatabaseName:"AdventureWorks2022" `
/SourceUser:"sqladmin" `
/SourcePassword:"$adminPassword" `
/TargetFile:"C:\Temp\AdventureWorks2022.bacpac"
# Upload to blob storage
az storage blob upload `
--account-name $storageAcct `
--container-name backups `
--file "C:\Temp\AdventureWorks2022.bacpac" `
--name "AdventureWorks2022.bacpac"
# Create Azure SQL DB server
$dbServer = "sql-migrate-$suffix"
az sql server create -g $resourceGroup -n $dbServer `
--admin-user sqladmin --admin-password $adminPassword -l $location
$myIp = (Invoke-RestMethod -Uri https://api.ipify.org)
az sql server firewall-rule create -g $resourceGroup --server $dbServer `
--name AllowMyIP --start-ip-address $myIp --end-ip-address $myIp
# Import BACPAC
$storageKey = az storage account keys list -g $resourceGroup -n $storageAcct --query '[0].value' -o tsv
az sql db import -g $resourceGroup -s $dbServer -n AdventureWorks2022 `
--storage-key-type StorageAccessKey `
--storage-key $storageKey `
--storage-uri "https://$storageAcct.blob.core.windows.net/backups/AdventureWorks2022.bacpac" `
--admin-user sqladmin --admin-password $adminPassword
BACPAC limitations: the source must be quiesced (no active transactions) during export. For databases > 200 GB, export can be very slow. There's no incremental β it's always a full export. For large DBs, use DMS instead.
Validate:
-- On Azure SQL DB
SELECT
s.name AS [Schema],
t.name AS [Table],
SUM(p.rows) AS [RowCount]
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
GROUP BY s.name, t.name
HAVING SUM(p.rows) > 0
ORDER BY [RowCount] DESC;
| Metric | Value |
|---|---|
| Downtime | Export + upload + import time (hours) |
| RPO | Zero (consistent export) |
| Complexity | Low |
| When to use | Small DBs (< 200 GB), Azure SQL DB target, acceptable downtime |
Scenario 3: Azure DMS Online Migration (to Azure SQL DB)β
Continuous data replication with minimal downtime cutover.
# Create DMS instance (Premium SKU required for online migrations)
az dms create `
--resource-group $resourceGroup `
--name "dms-migrate-$suffix" `
--location $location `
--sku-name Premium_4vCores
# Create target Azure SQL DB (empty β DMS migrates schema + data)
az sql db create -g $resourceGroup -s $dbServer -n AW2022_DMS --service-objective S2
For DMS online migration, the source database must be in FULL recovery model and must have had at least one full backup. The Premium DMS SKU is required for online mode β Standard only supports offline.
Then in the Azure Portal:
- Navigate to Azure Database Migration Service β your DMS instance
- Select + New Migration Project
- Source type: SQL Server, Target type: Azure SQL Database
- Activity type: Online data migration
- Configure source connection (to your
sqlvm-source) - Configure target connection (to your Azure SQL DB)
- Select tables to migrate
- Start the migration
- Monitor the Incoming changes counter β when it stabilizes near zero, trigger cutover
-- On source β verify CDC is capturing changes during sync
SELECT
name,
is_cdc_enabled
FROM sys.databases
WHERE name = 'AdventureWorks2022';
-- Make some changes during the online phase to test replication
INSERT INTO HumanResources.Department (Name, GroupName, ModifiedDate)
VALUES ('DMS Test Dept', 'Test Group', GETDATE());
-- This should appear on the target within seconds
| Metric | Value |
|---|---|
| Downtime | Minutes (cutover window only) |
| RPO | Near-zero (continuous CDC sync) |
| Complexity | Medium (DMS setup, network, CDC) |
| When to use | Any size DB to Azure SQL DB, minimal downtime required |
Scenario 4: Managed Instance Link (to SQL MI)β
The best online migration to SQL MI β uses Always On AG technology under the hood.
# Deploy MI for link target (if not already done in MI lab)
# See the MI Flavors lab Step 1 for full MI deployment
# Create the MI Link using Azure CLI
az sql mi link create `
--resource-group $resourceGroup `
--instance-name "mi-migrate-$suffix" `
--name "mi-link-aw" `
--primary-availability-group-name "SourceAG" `
--secondary-availability-group-name "TargetAG" `
--source-endpoint "TCP://sqlvm-source.yourdom.local:5022"
Requirements on the source SQL Server:
-- Source must be SQL Server 2019 CU17+ or 2022
SELECT @@VERSION;
-- Must enable HADR
-- (from PowerShell: Enable-SqlAlwaysOn -ServerInstance "sqlvm-source" -Force)
-- Database must be FULL recovery with a backup
ALTER DATABASE [AdventureWorks2022] SET RECOVERY FULL;
BACKUP DATABASE [AdventureWorks2022] TO DISK = 'NUL';
-- Create certificate for AG endpoint authentication
CREATE MASTER KEY ENCRYPTION BY PASSWORD = '<StrongPassword>';
CREATE CERTIFICATE MI_Link_Cert
WITH SUBJECT = 'MI Link Certificate';
-- Create mirroring endpoint
CREATE ENDPOINT AG_Endpoint
STATE = STARTED
AS TCP (LISTENER_PORT = 5022)
FOR DATABASE_MIRRORING (
AUTHENTICATION = CERTIFICATE MI_Link_Cert,
ROLE = ALL
);
MI Link is the preferred migration method for SQL MI because it uses AG technology β you get real-time sync with near-zero RPO. The cutover (breaking the link) takes seconds. The downside: requires SQL 2019 CU17+ or 2022 on source.
| Metric | Value |
|---|---|
| Downtime | Seconds (break link) |
| RPO | Near-zero (AG sync) |
| Complexity | Medium (AG endpoint, certificates) |
| When to use | SQL MI target, SQL 2019+, large databases, minimal downtime |
Scenario 5: Log Replay Service (to SQL MI)β
Backup-chain streaming to MI β works with any SQL Server version that supports backup to URL.
# Create storage account for LRS
$lrsStorage = "stlrs$suffix"
az storage account create -g $resourceGroup -n $lrsStorage -l $location --sku Standard_LRS
az storage container create --account-name $lrsStorage --name lrs-backups
# Get SAS token for MI to read backups
$expiry = (Get-Date).AddDays(7).ToString("yyyy-MM-ddTHH:mm:ssZ")
$sasToken = az storage container generate-sas `
--account-name $lrsStorage `
--name lrs-backups `
--permissions rl `
--expiry $expiry -o tsv
On the source:
-- Create credential for blob storage
CREATE CREDENTIAL [https://stlrsXXXX.blob.core.windows.net/lrs-backups]
WITH IDENTITY = 'SHARED ACCESS SIGNATURE',
SECRET = '<SAS_TOKEN>';
-- Full backup to blob
BACKUP DATABASE [AdventureWorks2022]
TO URL = 'https://stlrsXXXX.blob.core.windows.net/lrs-backups/AW2022_full.bak'
WITH COMPRESSION;
-- Log backup (do this periodically during migration)
BACKUP LOG [AdventureWorks2022]
TO URL = 'https://stlrsXXXX.blob.core.windows.net/lrs-backups/AW2022_log_001.trn'
WITH COMPRESSION;
Start LRS on the MI:
# Start Log Replay Service (continuous mode)
az sql midb log-replay start `
--resource-group $resourceGroup `
--managed-instance "mi-migrate-$suffix" `
--name "AdventureWorks2022" `
--storage-uri "https://$lrsStorage.blob.core.windows.net/lrs-backups" `
--storage-sas "$sasToken" `
--auto-complete false
# Check status
az sql midb log-replay show `
--resource-group $resourceGroup `
--managed-instance "mi-migrate-$suffix" `
--name "AdventureWorks2022"
Cutover:
# Stop source application, take final log backup
# (on source): BACKUP LOG [AdventureWorks2022] TO URL = '...' WITH NORECOVERY;
# Complete the restore on MI
az sql midb log-replay complete `
--resource-group $resourceGroup `
--managed-instance "mi-migrate-$suffix" `
--name "AdventureWorks2022" `
--last-backup-name "AW2022_log_final.trn"
| Metric | Value |
|---|---|
| Downtime | Minutes (final log backup + complete) |
| RPO | Last log backup interval |
| Complexity | Medium (backup chain management) |
| When to use | SQL MI target, any SQL version, cost-effective (no DMS needed) |
Scenario 6: Transactional Replication (to Azure SQL DB)β
Real-time replication with per-table control β great for phased migration.
-- On source β enable replication
-- (run SQL Server Agent if not running)
EXEC sp_replicationdboption
@dbname = 'AdventureWorks2022',
@optname = 'publish',
@value = 'true';
-- Create publication for selected tables
EXEC sp_addpublication
@publication = 'AW_Migration_Pub',
@status = 'active',
@allow_push = 'true',
@allow_pull = 'false';
-- Add articles (tables to replicate)
EXEC sp_addarticle
@publication = 'AW_Migration_Pub',
@article = 'Product',
@source_table = 'Product',
@source_owner = 'SalesLT',
@destination_table = 'Product',
@destination_owner = 'SalesLT';
-- Add more tables as needed
EXEC sp_addarticle @publication = 'AW_Migration_Pub',
@article = 'SalesOrderHeader', @source_table = 'SalesOrderHeader',
@source_owner = 'SalesLT', @destination_table = 'SalesOrderHeader',
@destination_owner = 'SalesLT';
-- Create subscription pointing to Azure SQL DB
EXEC sp_addsubscription
@publication = 'AW_Migration_Pub',
@subscriber = 'sql-migrate-XXXX.database.windows.net',
@destination_db = 'AW2022_Repl',
@subscription_type = 'push';
-- Start the snapshot agent (initial sync)
EXEC sp_startpublication_snapshot @publication = 'AW_Migration_Pub';
Monitor replication:
-- Check replication latency
EXEC sp_replmonitorsubscriptionpendingcmds
@publisher = @@SERVERNAME,
@publisher_db = 'AdventureWorks2022',
@publication = 'AW_Migration_Pub';
-- View replication status
SELECT
publication,
subscriber_db,
status,
last_distsync
FROM distribution.dbo.MSsubscriptions;
| Metric | Value |
|---|---|
| Downtime | Seconds (DNS switch after drain) |
| RPO | Near-zero (real-time replication) |
| Complexity | High (Distributor, Publisher, Subscriber setup) |
| When to use | Selective table migration, phased approach, SQL DB target |
Master Comparisonβ
After completing all scenarios, fill in your observations:
| Method | Target | Your Downtime | Your Transfer Rate | Difficulty (1-5) | Would Use For |
|---|---|---|---|---|---|
| Backup/Restore | SQL VM | ___ | ___ | 1 | ___ |
| BACPAC | SQL DB | ___ | ___ | 1 | ___ |
| DMS Online | SQL DB | ___ | ___ | 3 | ___ |
| MI Link | SQL MI | ___ | ___ | 3 | ___ |
| LRS | SQL MI | ___ | ___ | 2 | ___ |
| Replication | SQL DB | ___ | ___ | 4 | ___ |
Exam Cheat Sheetβ
Common Failuresβ
| Symptom | Likely Cause | Fix |
|---|---|---|
| DMA reports "unsupported feature" β e.g. cross-DB query | Target tier doesn't support that feature | Refactor query, switch tier (MI > DB), or pick different migration target |
| LRS restore stuck "COPYING" | Network bandwidth or large DB | Wait β ~150 GB/hr typical; monitor sys.dm_database_replica_states on target |
| MI Link replication breaks after planned failover | Distributed AG link not torn down before reverse seed | Drop link cleanly with ALTER AVAILABILITY GROUP ... REMOVE LISTENER first |
| BACPAC export fails on large DB | Single-threaded; chokes >200 GB | Switch to LRS or DMS for >100 GB workloads |
| Cutover window blown by long transactions on source | Final delta sync waited on long-running tx | Pre-drain long transactions; schedule cutover during low activity |
LRS is one-way and one-shot. Once the restore completes and you cut over, you cannot replicate back. If you need rollback capability, use MI Link (bidirectional) or transactional replication.
Variations to Tryβ
- Run DMA assessment first β produces compatibility report, blocks bad migrations early
- Compare online (MI Link, DMS) vs offline (BACPAC, LRS) for the same DB β measure cutover window
- Migrate the same DB to 3 different targets (DB, MI, IaaS) β compare cost, perf, feature parity
- Test rollback path: MI Link reverse seed, or pre-cutover backup of source
- Layer Azure Migrate for assessment if migrating dozens of DBs
Production Applicationβ
- Lift-and-shift, minimal change β LRS (Azure SQL DB), MI Link (Azure SQL MI), or VM-to-VM backup/restore.
- Online migration with sub-minute cutover β MI Link or transactional replication (DB has no native online option).
- Refactor allowed, modernize tier β BACPAC + DMA-driven schema fixes.
- Anti-pattern: don't pick BACPAC for >200 GB or transactional workloads β it's offline, single-threaded, and slow.
Clean Upβ
# Delete everything
az group delete --name $resourceGroup --yes --no-wait
What's Next?β
| Direction | Lab |
|---|---|
| Set up HA for your migrated SQL VM | AlwaysOn HA Setup |
| Add cross-region DR | Cross-Region AlwaysOn |
| Secure your migrated database | SQL Security Assessment |
| Configure DR for Azure SQL DB | Geo-Replication & Failover |