SQL Server Database Migration Pitfalls (and How to Fix Them)
Database migrations are where great product ideas go to die—unless you plan for SQL Server’s quirks and the realities of production load. Below are the most common mistakes I see, plus concise patterns to avoid them. Every item has a tiny, copy-pasteable example.
1) “Online” index changes that still block (and can’t be paused)
Even online operations need brief S/Sch-M locks; without guardrails they can block or be blocked.
Bad
Better — wait politely, bail out if needed, and make it resumable
2) Auto-created / auto-dropped indexes (Azure automatic tuning surprises)
If you use Automatic Tuning in Azure SQL Database, it may create and drop indexes. That’s great for performance—but surprising during migrations if you expect the schema to be “yours only.”
Bad
Better
Know what’s enabled (
CREATE/DROP INDEXautomatic tuning).Treat those indexes as ephemeral and do not bake strict dependencies on them into migrations.
In DACPAC deployments, don’t indiscriminately drop indexes not in source (see #3).
3) Letting your DACPAC drop things (or lose data) by surprise
Focus on avoiding destructive drops, especially for Users, Permissions, and indexes you didn’t model (incl. auto-tuning ones). Assume your team owns the database alone unless stated otherwise.
Baseline to aim for (teams that fully own their DB):
Explanation
BlockOnPossibleDataLoss=Trueaborts risky publishes.DropObjectsNotInSource=Truekeeps drift low but pair it with:DoNotDropObjectType=Users;Permissionsso principals and grants stay.DropIndexesNotInSource=Falseso you don’t drop indexes your dacpac didn’t model (incl. Azure’s automatic ones).
4) CDC tables break after schema changes
Change Data Capture doesn’t automatically follow new columns on captured tables.
Bad
Better — plan a new capture instance, then retire the old
5) Temporal tables altered in place
System-versioned tables need special handling; many schema changes require turning versioning off, updating both current and history, then turning it back on.
Bad
Better
6) Partition switching that isn’t aligned
Partition switching only works if the table and all aligned indexes share compatible partitioning.
Bad
Better
Ensure table and all indexes are aligned (same function/boundaries).
Then switch:
7) Long-running index maintenance on busy tables
Huge rebuilds during peak load will grow log/tempdb and hurt concurrency.
Bad
Better
Prefer targeted maintenance; rebuild only the indexes that need it.
Use ONLINE + RESUMABLE and a sensible
MAXDOP.
8) One-shot destructive changes
Dropping or renaming in the same release as the app change risks outages.
Bad
Better — two-phase, backward-compatible migrations
Deploy A: add new columns, dual-write, backfill, keep old ones.
Deploy B: switch reads, remove old columns when unused (and empty).
9) Using MERGE for upserts in critical migrations
MERGE has a long history of tricky edge cases (indexed views, OUTPUT, concurrency), and multiple documented defects.
Bad
Better — explicit, simpler statements
10) Deleting millions of rows under row-versioning (tempdb + log pain)
Under RCSI/SNAPSHOT, updates/deletes version rows into tempdb. A huge DELETE can explode tempdb and the log. If your intent is to purge, don’t row-by-row delete.
Bad
Better — truncate or switch, else batch
Partitioned table: use partition SWITCH out + fast TRUNCATE on the staging table.
Non-partitioned: delete in small batches with short transactions.
Prefer
TRUNCATE TABLEfor full-table purges (resets identity; minimal logging, but still fully logged meta-ops).
Quick checklist to bake into your pipeline
Block data loss and control drops in dacpac publishes (see #3).
Use ONLINE + WAIT_AT_LOW_PRIORITY and RESUMABLE for index work.
Treat Azure Automatic Tuning indexes as “non-authoritative.”
Plan for CDC and Temporal special cases.
Prefer batching, TRUNCATE, or partition switching over huge deletes.
Avoid
MERGEin mission-critical migrations; use clearerUPDATE+INSERT.

