Migration guides
MySQL to SQL Server migration guide
Convert a MySQL or MariaDB dump into a SQL Server script with the same tables, keys, indexes and data: how to dump, convert, load, and every rule the converter applies.
IntoSQL reads a MySQL or MariaDB dump and writes one SQL Server script with the same tables, columns, keys, indexes, foreign keys, defaults, identities, comments and rows. It reads the dump itself, without a database server, and lists in the report every value SQL Server can't hold as it is and every object it doesn't carry. The script loads on SQL Server 2017 to 2025.
Convert in the browser: MySQL to SQL Server opens the converter set up for this pair.
Dump for the best result
Any dump from mysqldump, mariadb-dump, mysqlpump, phpMyAdmin or Adminer works, as .sql or .sql.gz. These options keep more of your data:
mysqldump --default-character-set=utf8mb4 --hex-blob \
--routines --triggers --events --single-transaction \
shop | gzip > shop.sql.gz--default-character-set=utf8mb4keeps emoji and other 4-byte characters. A dump made withutf8has already lost them, and the report says so (dump_lost_four_byte_chars).--hex-blobwrites binary columns as hexadecimal, which survives copying between systems.--routines --triggers --eventsadd those objects to the dump, so the report can list them by name.--single-transactiontakes a consistent snapshot of InnoDB tables without locking them.
A dump made with --no-create-info still converts: the types are read from the values, and the report says the schema was missing (no_schema).
Convert
In the browser, open MySQL to SQL Server or the converter on the home page, drop the dump and choose SQL Server. It reads the file from your disk in pieces, twice, and writes the script as it goes, so the dump is never uploaded and its size isn't limited by memory. The desktop app does the same with the file on disk.
With the command line:
intosql inspect shop.sql.gz
intosql convert shop.sql.gz --to sql_server --output shop.sql_server.sql --report shop.report.jsoninspect lists the tables, their rows and what won't be converted, without writing anything. convert writes the script and, with --report, the report as JSON.
Through the API, a dump converts like any other file: open a conversion with its name ending in .sql or .sql.gz and "output": {"dialect": "sql_server"}.
Load into SQL Server
sqlcmd -S localhost -Q "CREATE DATABASE shop"
sqlcmd -b -f 65001 -S localhost -d shop -i shop.sql_server.sql-b stops at the first error and -f 65001 reads the script as UTF-8. The script creates the tables, copies the rows in batches of up to 1000 with IDENTITY_INSERT, then adds indexes, foreign keys, identities, triggers and comments. Foreign keys are added WITH NOCHECK and then checked: a row that points to a missing row is named by its constraint instead of failing the load half way. Each identity continues after the highest id copied, or after the table's AUTO_INCREMENT when that is higher.
What isn't converted
Views, triggers, stored procedures, functions and events are written in each database's own language. They are listed in the report by kind and name, never dropped silently. ON UPDATE CURRENT_TIMESTAMP is the exception: it becomes a SQL Server trigger.
Data types
MySQL to SQL Server data types lists each type that changes in the script: what it becomes, why, and the report line.
Every other rule
Generated from the engine, where each rule is decided.
| In MySQL | In SQL Server | Why | Report line |
|---|---|---|---|
| NOT NULL columns with values that become NULL | NOT NULL dropped | A column whose values had to become NULL can't keep NOT NULL, or the load would stop. | not_null_relaxed |
| text decoded twice (ñ) | copied as it is | The dump holds exactly these characters, so they are copied; the report points them out. | mojibake_suspected |
| ON UPDATE CURRENT_TIMESTAMP | an AFTER UPDATE trigger | The trigger sets the column to the current time when an update doesn't set it. It joins on the primary key, so the table needs one. | on_update_trigger |
| ON UPDATE CURRENT_TIMESTAMP on a table without a primary key | not carried | The trigger finds the updated rows by their primary key; without one it can't. | on_update_without_key |
| generated columns | computed columns, PERSISTED when the source stores them | SQL Server computes them the same two ways. | None |
| generated columns with functions SQL Server lacks | a plain column | A formula that can't be written for SQL Server leaves a plain column and a report line. | generated_not_translated |
| INVISIBLE columns | regular columns | SQL Server has no invisible columns. | invisible_column |
| CHECK constraints | translated, inline in CREATE TABLE | Inline checks are tested as each row loads. | None |
| CHECK constraints with functions SQL Server lacks | not created | A check that can't be written for SQL Server is left out and reported. | check_skipped |
| CHECK ... NOT ENFORCED | not created | MySQL keeps these checks without applying them, so the rows may break them. | check_not_enforced |
| defaults with functions SQL Server lacks | no default | A default that can't be written for SQL Server is left out and reported. | default_not_translated |
| keys and indexes on a prefix, c(20) | an index on the whole column | SQL Server indexes whole columns; a key unique on a prefix is unique on the whole column too, so every row loads. | prefix_whole_column |
| FULLTEXT indexes | not created | SQL Server's full-text search needs a catalog and its own queries. | fulltext_not_created |
| SPATIAL indexes | not created | A spatial index on a geometry column needs a bounding box the dump doesn't state. | spatial_index_not_created |
| functional indexes, and indexes on columns of unlimited length | not created | SQL Server indexes neither expressions nor nvarchar(max) and varbinary(max) columns. | index_skipped |
| foreign keys | after the data WITH NOCHECK, then checked with a warning on orphans | Dumps load with FOREIGN_KEY_CHECKS=0 and MyISAM never checks, so rows without a parent are common. The key is created first and checked last; orphans leave it unchecked with a warning instead of stopping the load. | None |
| ON DELETE or ON UPDATE that cascades in a cycle or along a second path | NO ACTION | SQL Server refuses cascades that could reach a table twice; MySQL allows them. | foreign_key_action_dropped |
| foreign keys to columns without a unique key | not created | SQL Server needs a primary key or a unique index without NULLs on the referenced columns; InnoDB doesn't. | foreign_key_skipped |
| table and column names | as written; renamed when they differ only in case | SQL Server names ignore case under the usual collation. | identifier_renamed |
| names over 116 characters | shortened | SQL Server names hold 128 characters, some of them for the numbers that keep names apart. | name_shortened |
| PARTITION BY | one table | The rows are the same; partition the table again in SQL Server if it needs it. | partitions_dropped |
| WITH SYSTEM VERSIONING (MariaDB) | a table with the current rows | A dump holds the current rows only. | system_versioning_dropped |
| MyISAM, MEMORY and other engines | a regular table | SQL Server has one table engine. | engine_dropped |
| views, triggers, procedures, functions and events | listed in the report | Their code is written in MySQL's language, so they are listed for you to recreate. | not_converted_objects |
| dumps without CREATE TABLE | types read from the values | A dump made with --no-create-info only has rows, so the types come from the values. | no_schema |
| _ci collations | the database's collation, usually case-insensitive | SQL Server compares text without case under its usual collation, as MySQL's _ci does. | None |
| SET NAMES utf8 from utf8mb4 tables | converted, with a warning | MySQL 5.x mysqldump writes 3-byte utf8 by default and replaces every emoji with a question mark before any converter sees the file. | dump_lost_four_byte_chars |
| dumps without SET NAMES whose text isn't UTF-8 | read as Latin-1 (Windows-1252) | MySQL's latin1 is Windows-1252, the usual encoding of such dumps. | charset_guessed |
| sjis, cp932, gbk, big5 dumps | read as UTF-8, with a warning | These encodings are not decoded yet. | unsupported_charset |
| UNHEX('..'), CONVERT(b'..', UNSIGNED), CONVERT(".." USING x), ST_GeomFromText('..') in rows | evaluated | Some dump tools write values as these calls; they are read as the values they produce, with geographic WKT turned to longitude first. | None |
| other expressions in rows | NULL | A converter that doesn't run a database can't evaluate them. | expression_value |
| DEFAULT in VALUES | the column's default value | A literal default is copied into the row. | default_keyword_filled |
| DEFAULT in VALUES without a literal default | NULL | There is no value to copy. | default_keyword_null |
| REPLACE and INSERT IGNORE | plain rows | A dump has one row per key, so the rows are the same. | replace_as_insert |
| CREATE SEQUENCE (MariaDB) | CREATE SEQUENCE ... AS bigint, at its next value | SQL Server has sequences with the same options; each one continues where the dump left it. | None |