Migrate MySQL to SQL Server
Beta — live-tested pathMigrating from MySQL (the world's most deployed open-source relational database) to SQL Server (Microsoft's enterprise relational database) means every table, column type, key, index and row has to survive two engines' different opinions about data. DBShifts converts the schema with deterministic rules, transfers data in parallel batches, and validates the result with per-table row counts and type-aware checksums.
Free tier · no credit card
MySQL vs SQL Server
MySQL
the world's most deployed open-source relational database.
SQL Server
Microsoft's enterprise relational database.
How the migration works
Connect
Point DBShifts at your source and target. Credentials stay encrypted; SSH tunnels supported for private databases.
Analyze
Automatic schema introspection produces a migration plan: what converts automatically, what migrates with warnings, what needs human review.
Migrate
Schema is created on the target, data transfers in parallel batches with constraints deferred, indexes rebuilt after load.
Validate
Per-table row counts, type-aware checksums on both sides, FK integrity — plus a fidelity report listing anything lossy.
MySQL to SQL Server data type mapping
Pulled directly from DBShifts's own conversion rules — not a general reference table, this is what actually runs.
| MySQL | SQL Server |
|---|---|
| INT | INT |
| BIGINT | BIGINT |
| SMALLINT | SMALLINT |
| TINYINT(1) | BIT |
| DECIMAL(10,2) | DECIMAL(10,2) |
| FLOAT | FLOAT |
| DOUBLE | FLOAT(53) |
| VARCHAR(255) | NVARCHAR(255) |
| TEXT | NVARCHAR(MAX) |
| LONGTEXT | NVARCHAR(MAX) |
| CHAR(10) | NCHAR(10) |
| DATE | DATE |
| DATETIME | DATETIME2 |
| TIMESTAMP | DATETIMEOFFSET |
| TIME | TIME |
| YEAR | SMALLINT |
| BLOB | VARBINARY(MAX) |
| JSON | NVARCHAR(MAX) |
| ENUM('a','b') | NVARCHAR(255) |
| SET('a','b') | NVARCHAR(MAX) |
Example: MySQL to SQL Server schema conversion
MySQL
CREATE TABLE orders (
id INT AUTO_INCREMENT PRIMARY KEY,
customer_id INT NOT NULL,
status ENUM('new','paid','shipped') NOT NULL DEFAULT 'new',
total DECIMAL(10,2) NOT NULL,
is_gift TINYINT(1) NOT NULL DEFAULT 0,
notes TEXT,
metadata JSON,
placed_at DATETIME NOT NULL
);SQL Server
IF OBJECT_ID(N'orders', 'U') IS NULL
CREATE TABLE [orders] (
[id] INT NOT NULL,
[customer_id] INT NOT NULL,
[status] NVARCHAR(255),
[total] DECIMAL(10,2) NOT NULL,
[is_gift] BIT,
[notes] NVARCHAR(MAX),
[metadata] NVARCHAR(MAX),
[placed_at] DATETIME2 NOT NULL,
CONSTRAINT [PK_orders] PRIMARY KEY ([id])
);BIT cannot hold what TINYINT was holding
The type table on this page maps TINYINT(1) to BIT, which is right for the boolean case and wrong for everything else. SQL Server's BIT stores 0, 1 or NULL — nothing else. A MySQL column declared TINYINT(1) can legally hold any value from -128 to 127, and applications do use it that way for small counters and status codes. Anything outside 0/1 fails to convert, and it fails per row rather than up front.
The only thing distinguishing a boolean from a small integer in MySQL is the display width, which is advisory. Before converting, run SELECT DISTINCT on every TINYINT(1) column and look at the actual values. If a column holds 2, it is not a boolean regardless of how it was declared, and it belongs in a TINYINT on the SQL Server side.
utf8mb4 into VARCHAR loses characters silently
MySQL's utf8mb4 stores the full Unicode range, including emoji and the CJK extension planes. SQL Server's VARCHAR is single-byte under a legacy collation, so a direct VARCHAR target replaces anything outside the code page with a question mark. There is no error. The load succeeds, the row counts match, and the data is damaged.
The fix is NVARCHAR, which is UTF-16 and covers the same range. On SQL Server 2019 and later a UTF-8 collation on VARCHAR also works and costs less space for mostly-ASCII data. What does not work is assuming the default collation will cope, which is the usual path to discovering this months later in a customer's name.
This is worth checking against real data rather than the schema, because the damage is invisible in a row count and in a checksum taken after conversion. Compare against the source.
MySQL allows many NULLs in a unique index; SQL Server allows one
This is the difference that breaks the load rather than the data. A MySQL unique index treats NULLs as distinct, so a nullable unique column can hold any number of NULL rows. A SQL Server UNIQUE constraint treats NULLs as equal, so the second NULL is a duplicate key violation.
A table that was perfectly valid in MySQL therefore cannot be loaded into a straight translation of its own schema. The usual answer is a filtered unique index (WHERE column IS NOT NULL), which reproduces the MySQL behaviour exactly. Deciding that after the load has already failed halfway through is considerably less pleasant than deciding it before.
MySQL → SQL Server conversion notes
Engine-specific rules baked into the conversion and transfer pipeline — verified by live certification of this exact pair.
- TINYINT(1) is treated as a boolean; plain TINYINT stays numeric — converting both blindly corrupts ratings and counters.
- UNSIGNED integers are widened on targets without unsigned types, so values above the signed range survive.
- ENUM and SET columns are converted with their value lists preserved (native enums, CHECK constraints, or arrays depending on the target).
- TIME columns are durations (-838h..838h) at the driver level — DBShifts converts them per target instead of failing the batch.
- Zero dates (0000-00-00) are normalized deterministically and reported, never silently dropped.
- IDENTITY_INSERT is managed per table so explicit IDs from the source load correctly, then counters are reseeded with DBCC CHECKIDENT.
- Binary columns always carry an explicit length — bare VARBINARY (which means 1 byte) never reaches the target.
- TRUNCATE falls back to DELETE on FK-referenced tables automatically.
Frequently asked questions
Is MySQL to SQL Server migration production-ready in DBShifts?
MySQL to SQL Server is a live-verified beta pair: it passed the same certification suite as every other pair — a real migration with edge-case data validated by row counts and checksums — and ships with a post-migration fidelity report you can audit before cutover.
How does DBShifts verify the MySQL to SQL Server migration was correct?
Three layers: per-table row counts on both sides, an order-independent type-aware checksum of row data (canonicalized so engine representation differences don't false-alarm), and FK integrity checks on the target. Rows that fail to insert are quarantined with the exact error — never silently dropped — and can be fixed and retried from the UI.
What happens to MySQL types that SQL Server doesn't have?
They convert by deterministic rules with a recorded decision: a native equivalent where one exists, a portable fallback (TEXT/JSON/DECIMAL) where one doesn't. Every lossy conversion appears in the migration plan before you run and in the fidelity report after. You can override any mapping per column.
Can I keep MySQL and SQL Server in sync after the initial migration?
Yes. DBShifts supports incremental sync (UPSERT-based delta transfers on a sync key) and, for supported sources, real-time change data capture so the target stays current until you cut over.
Do I need to write any SQL or scripts for MySQL to SQL Server?
No. Connect both databases, review the generated migration plan (what's automatic, what migrates with warnings, what needs manual review — typically triggers and stored procedures), and run. Procedural code is transpiled best-effort and queued for human review rather than auto-applied.
What's the hardest part of moving MySQL to SQL Server?
Type-system mismatches that fail silently rather than loudly — MySQL's edge-case types (5 engine-specific rules apply) converting into SQL Server's nearest equivalent. DBShifts's deterministic mapping records every downgrade in the fidelity report, so a value that clips, rounds, or widens is visible before cutover, not after.
Which MySQL data types change when moving to SQL Server?
9 of the mapped types convert to something that cannot represent exactly the same range or constraint. The clearest cases are TINYINT(1) to BIT, FLOAT to FLOAT, DOUBLE to FLOAT(53). The full table above lists every mapping the converter performs, and analysis flags each one on your actual schema before any data moves.
Go deeper
Related migration paths
Ready to move MySQL to SQL Server?
Set up in under two minutes. Validation and rollback included.
Start Free Migration