Skip to content
adapters.io

Convert SQL Server data types to MySQL with the full type mapping and a pre-flight query for every risky type

10 min read Migration The Adapters team

Field mapping auto-plugged · tap a port to rewire

5 sample records ready

Most SQL Server to MySQL type conversions are obvious: INT stays INT, DATE stays DATE, a DECIMAL with a declared precision stays exact. About ten are not, and they produce a schema that deploys cleanly and loads every row while quietly changing what the data means. The full mapping is below, followed by the part converters never ship: the one-line query to run on SQL Server that proves each risky mapping safe before you load anything.

Key takeaways

  • SQL Server TIMESTAMP is not a date. Microsoft documents it as a synonym for rowversion, an 8-byte counter. MySQL Workbench's published mapping still sends it to MySQL TIMESTAMP.
  • A bare DECIMAL has no cents. MySQL treats DECIMAL with no arguments as DECIMAL(10,0). MONEY needs DECIMAL(19,4), written out.
  • datetime2 has one digit more than MySQL can hold. Seven fractional digits against six, and zero if the precision is left off.
  • Collation changes what counts as equal. MySQL 8.4's default is accent insensitive and treats trailing spaces as significant. SQL Server's US default does the opposite on both.
  • Check on the source, reconcile on values. Every problem here is one query on SQL Server and a quarter of cleanup once it reaches production.

How do I convert SQL Server data types to MySQL?

Generate a draft schema with a converter, then override it for the columns where the two engines disagree about meaning. AWS SCT and the MySQL Workbench Migration Wizard both produce a workable first draft, and most of it you will keep. The work is not writing a mapping from scratch. It is finding the ten or so defaults to reject, each of which produces valid DDL and a successful load, which is exactly why they survive testing.

It helps to sort the types into three groups. The first is identical or nearly so: the integer family, DECIMAL with arguments, DATE, BINARY and VARBINARY. The second is a mechanical adjustment because the engines size things differently: TINYINT needs UNSIGNED, FLOAT needs DOUBLE, the MAX types become LONGTEXT and LONGBLOB. The third is where the meaning changes: MONEY, the date and time family, rowversion, uniqueidentifier and anything a collation touches. The third group is the whole job.

SQL Server to MySQL data type mapping table

Every row was checked against Microsoft Learn, the MySQL 8.4 Reference Manual and the MySQL Workbench type mapping on 30 September 2026.

SQL Server type MySQL type Note
BIT TINYINT(1) or BOOLEAN BOOLEAN is an alias for TINYINT(1) in MySQL. BIT(1) returns a byte string to many drivers.
TINYINT TINYINT UNSIGNED SQL Server TINYINT is 0 to 255. A signed MySQL TINYINT stops at 127.
SMALLINT, INT, BIGINT SMALLINT, INT, BIGINT Signed ranges match exactly. These are clean.
DECIMAL(p,s), NUMERIC(p,s) DECIMAL(p,s) Exact in both. Always carry the precision and scale across.
MONEY DECIMAL(19,4) A bare DECIMAL in MySQL means DECIMAL(10,0), with no decimal places.
SMALLMONEY DECIMAL(10,4) Same trap, smaller range.
FLOAT(53) DOUBLE SQL Server FLOAT defaults to 8 bytes. A bare MySQL FLOAT is 4 bytes, so a mapping that drops the precision loses digits.
REAL FLOAT Both are 4-byte floating point.
CHAR(n), VARCHAR(n) CHAR(n), VARCHAR(n) in utf8mb4 Watch the row budget: utf8mb4 reserves up to 4 bytes per character.
NCHAR(n), NVARCHAR(n) CHAR(n), VARCHAR(n) in utf8mb4 MySQL has no separate national types worth using. The column character set does the work.
VARCHAR(MAX), NVARCHAR(MAX) LONGTEXT TEXT types sit off page and cost only 9 to 12 bytes of the row limit.
TEXT, NTEXT LONGTEXT Both are deprecated in SQL Server anyway.
BINARY(n), VARBINARY(n) BINARY(n), VARBINARY(n) Direct equivalents at small sizes.
VARBINARY(MAX), IMAGE LONGBLOB One target for both.
DATE DATE Clean, except values before 1000-01-01, below MySQL's supported range.
TIME(7) TIME(6) The seventh fractional digit rounds.
DATETIME DATETIME(3) SQL Server DATETIME keeps milliseconds. A MySQL DATETIME with no precision keeps none.
SMALLDATETIME DATETIME Minute precision in SQL Server, so no fractional digits are needed.
DATETIME2(7) DATETIME(6) 100 ns accuracy in SQL Server, 1 microsecond at best in MySQL.
DATETIMEOFFSET DATETIME(6) in UTC, plus an offset column No MySQL type stores the offset with the value.
TIMESTAMP, ROWVERSION BINARY(8), or drop the column An 8-byte counter, not a date. Never MySQL TIMESTAMP.
UNIQUEIDENTIFIER BINARY(16) or CHAR(36) BINARY(16) indexes smaller. CHAR(36) is readable.
XML LONGTEXT MySQL has no XML type. TEXT caps at 64 KB.
SQL_VARIANT, HIERARCHYID Redesign The Workbench mapping lists both as not migrated.
GEOGRAPHY, GEOMETRY GEOMETRY with an SRID Supported, but convert through well-known text, not a byte copy.

What is the MySQL equivalent of SQL Server timestamp?

There isn't one, because SQL Server's timestamp is not a point in time. Microsoft's documentation is blunt about it: timestamp is the synonym for rowversion, a data type that exposes "automatically generated, unique binary numbers", and it "does not preserve a date or a time." A non-nullable rowversion is equivalent to binary(8). Applications use it for optimistic concurrency, reading the value with a row and refusing the update if it has changed since.

The type mapping published in the MySQL Workbench manual sends both TIMESTAMP and ROWVERSION to MySQL TIMESTAMP, which is a date type with a range that ends in January 2038. The mapping matches on the name, and the name is a historical accident Microsoft has deprecated. What happens next depends on the target's SQL mode. Under MySQL's default strict mode the value is rejected. With strict mode off, it is stored as a zero date. Neither outcome keeps the concurrency check working.

Find these columns with one query against sys.columns, listed in the table further down. If the application still reads the value, map it to BINARY(8) and have the application maintain it, because MySQL will not increment it for you. If nothing reads it, drop it and add an updated_at DATETIME(6) column, which is the MySQL idiom for the same job.

How do I convert SQL Server MONEY to MySQL?

Declare DECIMAL(19,4) and write both numbers out. SQL Server MONEY holds four decimal places. The MySQL manual says that when DECIMAL is declared without arguments, the scale is 0 and "the default value of M is 10", which makes a bare DECIMAL a whole number of at most ten digits. The Workbench mapping table lists MONEY and SMALLMONEY as "DECIMAL" with no precision shown, so read the DDL your tool actually generates rather than the table.

This one hides well. Rows load, counts match, and totals look plausible because the errors are under a dollar each. The difference appears when someone ties the migrated ledger back to the source and finds it out by a few thousand dollars across a year. If those balances drive reminders to customers, the damage is worse than a report, because chasing unpaid invoices for the wrong amount is the fastest way to lose the customer you are trying to collect from. Compare SUM(col) per money column on both sides, to the cent, before sign-off.

What is the MySQL equivalent of datetime2?

DATETIME(6). Microsoft documents datetime2 with a default precision of 7 digits and an accuracy of 100 nanoseconds. MySQL's fractional seconds stop at 6 digits, so the seventh is rounded on every value. That is usually harmless. The value that is not harmless is a DATETIME declared with no precision at all, which is MySQL's default and which keeps zero fractional digits.

With zero digits every value rounds to the nearest second, so anything at 23:59:59.5 or later moves into the next day. On 31 December that means the next month, the next quarter and the next fiscal year. The count of such rows is tiny, which is why reconciliation by row count never finds them, and it is exactly the handful of late-night orders that decides whether a year-end report agrees with the old system.

Two related checks belong here. First, .NET's DateTime.MinValue is 0001-01-01, and applications written against datetime2 often store it as a placeholder, while MySQL supports DATETIME only from 1000-01-01. Second, DATETIMEOFFSET has no MySQL equivalent that keeps the offset. Microsoft documents that converting it to a type without an offset copies the date and time and truncates the time zone, so a row written in New York and one written in Los Angeles at the same instant end up three hours apart. Convert to UTC during the load and keep the original offset in its own column if anything downstream needs local time.

What is the MySQL equivalent of uniqueidentifier?

BINARY(16) if index size matters, CHAR(36) if people read the values. Either is fine. The Workbench mapping uses VARCHAR(64) and notes "Unique flag set in MySQL", which is right for a GUID primary key and wrong for a GUID foreign key, where the same customer id repeats on every order by design. Check each generated column against its role before the load, because a unique index on a foreign key rejects valid rows and a loader running with INSERT IGNORE drops them without a word.

If you choose BINARY(16), convert with MySQL's UUID_TO_BIN() on the way in and BIN_TO_UUID() on the way out, and test the byte order against a few known values. SQL Server stores and sorts uniqueidentifier bytes in its own order, so a naive byte copy produces values that look valid and match nothing.

Why do my MySQL queries return different rows than SQL Server?

Collation, in two ways teams rarely expect. MySQL 8.4 defaults to utf8mb4 with utf8mb4_0900_ai_ci, where ai means accent insensitive. A US-English SQL Server installs SQL_Latin1_General_CP1_CI_AS, which Microsoft describes as case insensitive and accent sensitive. José and Jose were two values in SQL Server and are one value in MySQL, so a unique index that was valid on the source fails to build on the target, or the second row is dropped.

The second is trailing spaces. Microsoft states that SQL Server pads strings before comparing them, so 'abc' and 'abc ' are equivalent for most comparisons. MySQL's manual states that collations based on UCA 9.0.0 and later, the default included, are NO PAD, which makes trailing spaces significant. A product code imported from a fixed-width file with a trailing space matched every query in SQL Server and matches none in MySQL. There is no error, only an empty result. The detection query uses LIKE, because Microsoft notes that LIKE is the one comparison it does not pad.

How do I verify a SQL Server to MySQL type conversion?

Run the checks on SQL Server before the load, not on MySQL after it. Every problem on this page is a cheap query against the source and an expensive incident in production. This is the list we work through. It takes about an hour on a schema of any size, because most of it reads from the system catalog rather than scanning data.

Risk Run this on SQL Server How to read the answer
A rowversion is about to become a date SELECT OBJECT_NAME(object_id), name FROM sys.columns WHERE TYPE_NAME(user_type_id) = 'timestamp'; Every row returned is a version counter. Map it to BINARY(8) if the application checks it on update, or drop it and add an updated_at DATETIME(6).
Money has sub-cent values SELECT COUNT(*), MAX(ABS(col)) FROM t WHERE col <> ROUND(col, 2); A non-zero count means the fourth decimal place is real data, so DECIMAL(19,4) is required rather than (19,2). The maximum tells you whether the integer part fits.
datetime2 carries sub-microsecond digits SELECT COUNT(*) FROM t WHERE DATEPART(nanosecond, col) % 1000 <> 0; These values will round in MySQL. Usually harmless, unless the column is part of a unique key, where two distinct values can round to the same one.
Rounding will cross midnight SELECT COUNT(*) FROM t WHERE CAST(col AS time) >= '23:59:59.5'; Rows that move to the next day, month or year if anything declares the column as DATETIME with no precision. Revenue on 31 December is the case finance notices.
Dates below MySQL's supported range SELECT COUNT(*) FROM t WHERE col < '1000-01-01'; .NET writes 0001-01-01 as DateTime.MinValue, so placeholder dates are common in datetime2 columns. MySQL supports DATETIME from 1000-01-01. Decide on NULL or a real date.
More than one time zone offset SELECT DATEPART(TZOFFSET, col), COUNT(*) FROM t GROUP BY DATEPART(TZOFFSET, col); One row back means one offset, and a plain DATETIME is safe. Several rows mean the offset carries meaning, and it must be converted to UTC and stored.
Accents will collide in a unique index SELECT col COLLATE Latin1_General_CI_AI, COUNT(*) FROM t GROUP BY col COLLATE Latin1_General_CI_AI HAVING COUNT(*) > 1; An approximation of MySQL's utf8mb4_0900_ai_ci. Any row returned is a pair that SQL Server kept apart and MySQL will call a duplicate.
Trailing spaces will stop matching SELECT COUNT(*) FROM t WHERE col LIKE '% '; LIKE is the one comparison Microsoft says does not pad, so it finds the values that equality hides. Trim them on load or choose a PAD SPACE collation.
Tables too wide for a MySQL row SELECT OBJECT_NAME(object_id), SUM(CASE WHEN max_length = -1 THEN 12 WHEN TYPE_NAME(system_type_id) IN ('nvarchar','nchar') THEN max_length * 2 WHEN TYPE_NAME(system_type_id) IN ('varchar','char') THEN max_length * 4 ELSE max_length END) FROM sys.columns GROUP BY object_id ORDER BY 2 DESC; A rough utf8mb4 byte budget per table. Anything above 65,535 fails with error 1118, so move the longest strings to TEXT before the CREATE TABLE.
Identity values and steps SELECT OBJECT_NAME(object_id), increment_value, last_value FROM sys.identity_columns; last_value is where each AUTO_INCREMENT must start after the load. Any increment other than 1 has no per-table equivalent in MySQL.

After the load, reconcile on values rather than counts. For each table compare the row count, the SUM of every numeric column, the MIN and MAX of every date column, and daily totals around midnight UTC. Then set every AUTO_INCREMENT above the loaded maximum and insert one test row per table inside a transaction you roll back. A green row-count report is the single most misleading artifact in this kind of project, because it passes on a migration that rounded every cent.

One piece of scope belongs in the plan rather than in a surprise. Types are the easy half. T-SQL procedures, functions and triggers need a code converter or a person, and AWS notes that MySQL has no MERGE statement and no multistatement table-valued functions, both of which SCT emulates. Budget that work separately.

The tool comparison for this route, with what each one bills by, is on our SQL Server to MySQL migration tools page. If PostgreSQL is also on the shortlist, the same source is covered in the SQL Server to PostgreSQL migration guide and the procedural side in converting T-SQL to PostgreSQL. For a warehouse target the same source types are mapped in convert SQL Server data types to Snowflake. The same DATETIME precision default catches document sources too, as the MongoDB to MySQL migration tools page shows, and the budget model for either program is in what a data migration really costs.

Declare the MySQL types once and keep them in sync with SQL Server

Map the columns with DATETIME(6), DECIMAL(19,4) and BINARY(16) where they belong, run the backfill, then let the same mapping run incrementally with retries, alerts and per-record logs. From $49 a month, never metered by rows.

The live demo needs no card, and Starter is $49 a month.

Get started