SQL Server On-Premises to Azure SQL Migration Guidance¶
This document provides best practices for migrating SQL Server databases from on-premises to Azure SQL, including assessment, planning, and cutover strategies.
Migration Assessment¶
Database Compatibility¶
Assess your current SQL Server version and target Azure SQL capabilities:
| Feature | SQL 2016 | SQL 2019 | Azure SQL | Notes |
|---|---|---|---|---|
| Contained databases | Supported | Supported | Supported | Recommended for multi-tenancy |
| In-memory OLTP (Hekaton) | Limited | Full | Limited | Check Azure SQL tier |
| Query Store | Supported | Supported | Supported | Essential for performance tuning |
| Temporal tables | Supported | Supported | Supported | Track data changes over time |
Workload Classification¶
Categorize databases by complexity:
- Lift & Shift: Minimal code changes, standard compatibility
- Modernize: Add cloud-native features (elastic pools, geo-replication)
- Refactor: Break monolith into microservices; use Azure SQL Database
Performance Baselines¶
Before migration, capture production metrics:
- Query execution times (P50, P95, P99)
- CPU, memory, I/O utilization per database
- Connection pool size and frequency
- Backup/restore times
- Transaction throughput (TPS)
Azure SQL Target Selection¶
Azure SQL Database vs Managed Instance¶
| Aspect | SQL Database | Managed Instance |
|---|---|---|
| Deployment | Single DB or elastic pool | Fully managed VM instance |
| Compatibility | T-SQL subset (99%+) | Near 100% on-premises |
| Cost | Pay per database or eDTU | Higher, closer to on-prem |
| Multi-database | Elastic pools group DBs | All DBs on single instance |
| Integration | Native Azure services | Some limitations |
Tier Selection Strategy¶
Single Database Tiers:
- DTU Model: Basic (5 DTU), Standard (10-3000 DTU), Premium (125-4000 DTU)
- vCore Model: General Purpose (2-80 vCore), Business Critical (2-80 vCore), Hyperscale (up to 128 vCore)
Elastic Pool: When managing 3+ databases with variable workloads
- Pool eDTU: 50-4000
- Per-database: 0-3000 eDTU
Right-size by analyzing:
- Peak CPU utilization across all databases
- Storage growth trajectory (1yr historical)
- Connection concurrency
- Expected growth in next 12 months
Migration Approach¶
Offline Migration (Minimal Downtime)¶
Best for single-database migrations where brief downtime acceptable:
- Full backup of source database
- Restore to Azure SQL
- Validate schema and data integrity
- Redirect application connection strings
- Monitor for 24-48 hours
- Decommission source
Downtime: 30 minutes to 2 hours
Online Migration (Zero Downtime)¶
Use Azure Database Migration Service (DMS) for minimal application downtime:
# Create migration project in DMS
$migrationProject = New-AzDataMigrationProject `
-ResourceGroupName "prod-rg" `
-ServiceName "mig-service" `
-ProjectName "sql2019-to-sqldb" `
-Location "East US 2" `
-SourceType "SqlServer" `
-TargetType "AzureSqlDatabase"
# Start migration task with CDC
New-AzDataMigrationTask `
-ProjectName $migrationProject.Name `
-ServiceName "mig-service" `
-TaskName "migrate-orders-db" `
-SourceConnection (New-AzDmsConnInfo -ServerType SqlServer ...) `
-TargetConnection (New-AzDmsConnInfo -ServerType AzureSqlDatabase ...) `
-MigrationType "Online"
Downtime: < 1 second during final sync
Pre-Migration Validation¶
Object Compatibility Check¶
-- Identify deprecated features
SELECT * FROM sys.databases
WHERE name = 'YourDatabase'
AND compatibility_level < 150
-- Find deprecated syntax
DBCC CHECKDB (YourDatabase, NOINDEX)
Connection String Updates¶
Update application connection strings:
// Before: On-premises SQL Server
string connString = @"Server=sqlserver.company.local;Database=Orders;
Integrated Security=true;Encrypt=false";
// After: Azure SQL with authentication
string connString = @"Server=orders-db.database.windows.net;
Database=Orders;User Id=admin@orders-db;Password=<password>;
Encrypt=true;TrustServerCertificate=false;
Connection Timeout=30";
Identity and Access Configuration¶
Use managed identities to eliminate hardcoded credentials:
// Azure App Service with managed identity
var credential = new DefaultAzureCredential();
var tokenProvider = new DefaultAzureCredentialTokenProvider();
using (SqlConnection conn = new SqlConnection("Server=.database.windows.net;Database=Orders;"))
{
conn.AccessToken = tokenProvider.GetToken(
new TokenRequestContext(scopes: new[] { "https://database.windows.net/.default" }))
.Token;
conn.Open();
// Execute queries
}
Configure Azure SQL firewall rules:
# Allow Azure services
New-AzSqlServerFirewallRule `
-ServerName "orders-db" `
-ResourceGroupName "prod-rg" `
-FirewallRuleName "AllowAzureServices" `
-StartIpAddress "0.0.0.0" `
-EndIpAddress "0.0.0.0"
# Allow app service IP
New-AzSqlServerFirewallRule `
-ServerName "orders-db" `
-ResourceGroupName "prod-rg" `
-FirewallRuleName "AllowAppService" `
-StartIpAddress "20.45.67.89" `
-EndIpAddress "20.45.67.89"
Post-Migration Optimization¶
Query Performance Tuning¶
Leverage Query Store to identify slow queries:
-- Top 10 queries by total execution time
SELECT TOP 10
q.query_id,
qt.query_sql_text,
SUM(rs.avg_duration * rs.execution_count) AS total_duration_ms,
MAX(rs.max_duration) AS max_duration_ms,
SUM(rs.execution_count) AS exec_count
FROM sys.query_store_query q
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_runtime_stats rs ON q.query_id = rs.query_id
WHERE database_id = DB_ID()
GROUP BY q.query_id, qt.query_sql_text
ORDER BY total_duration_ms DESC
Add indexes where needed:
-- Analyze missing indexes
SELECT migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS improvement_measure,
mid.equality_columns, mid.inequality_columns, mid.included_columns
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups_stats migs ON mid.index_handle = migs.index_handle
WHERE mid.database_id = DB_ID()
ORDER BY improvement_measure DESC
Backup and Restore Strategy¶
Configure automatic backups and geo-replication:
# Enable geo-replication for disaster recovery
New-AzSqlDatabaseSecondary `
-ServerName "orders-db" `
-DatabaseName "Orders" `
-PartnerServerName "orders-db-geo" `
-ResourceGroupName "prod-rg"
# Monitor replication lag
Get-AzSqlDatabaseReplicationLink `
-ServerName "orders-db" `
-DatabaseName "Orders" `
-ResourceGroupName "prod-rg" |
Select-Object ReplicationState, ReplicationLag
Monitoring and Alerting¶
Set up Azure Monitor alerts for common issues:
# Alert when CPU exceeds 80%
$actionGroup = Get-AzActionGroup -ResourceGroupName "prod-rg" -Name "DBA-Team"
$metric = New-AzMetricAlertRuleV2 `
-Name "HighCPU-Orders-DB" `
-ResourceGroupName "prod-rg" `
-TargetResourceId "/subscriptions/.../resourceGroups/prod-rg/providers/Microsoft.Sql/servers/orders-db/databases/Orders" `
-MetricName "cpu_percent" `
-Operator GreaterThan `
-Threshold 80 `
-WindowSize 00:05:00 `
-Frequency 00:01:00 `
-ActionGroup $actionGroup.Id
Base Coat Assets¶
- Agent:
agents/azure-cloud-migrate.agent.md - Skill:
skills/azure-migrate/ - Instruction:
instructions/zero-trust-identity.instructions.md