Updated and tested 10 October 2026
Most of the time this question was settled years before anybody asked it. The application is PHP, so it talks to MySQL. The application is a .NET line-of-business system somebody's company bought in 2014, so it talks to SQL Server. The decision came bundled with the framework and nobody sat down with a feature grid.
Both engines are mature enough that neither of them will be the reason your application is slow. What separates them is the licence, the stack each one grew up inside and a handful of dialect details that break code silently during a port. This article is shorter than the PostgreSQL comparison because MySQL vs MSSQL has less to argue about.
Where MySQL and SQL Server each came from
MySQL's position comes from being the M in LAMP at the exact moment shared hosting exploded. PHP shipped with MySQL support, every hosting control panel offered a MySQL database as a checkbox and a generation of applications got written against it. WordPress is the extreme case: it targets MySQL, the plugins around it assume MySQL, and roughly speaking the entire content management layer of the web is a MySQL client. Network effects that large outlast most technical merits.
SQL Server's position comes from Windows Server and .NET. It was the database that came with the platform your company had already licensed, it integrated with Active Directory for authentication, and Microsoft built SSIS, SSRS and the analysis layer on top so the reporting department never had to leave. Twenty years of internal line-of-business applications sit on that foundation and none of them are moving.
A PHP application on SQL Server is possible and I've seen it work. The drivers are fine and the ORM support exists. It also means every Stack Overflow answer about your stack is written for a database you're not running, which costs more hours over a year than any benchmark difference will.
MySQL vs MSSQL on licensing and free tiers
MySQL: GPL community edition and Oracle's commercial editions
MySQL is owned by Oracle, which acquired it through Sun in 2010, and it's dual-licensed. MySQL Community Edition is GPLv2 and free, including the server and the client libraries, and it's what nearly everyone runs. Oracle also sells Standard, Enterprise and Cluster CGE editions with extras like MySQL Enterprise Firewall, Enterprise Encryption and data masking.
Oracle's OEM licensing page spells out who the commercial licence is for: companies that want to distribute commercial binaries of MySQL alongside their own proprietary software, without subjecting that software to the GPL and its source-distribution requirement, should buy one. There's a Universal FOSS Exception so non-GPL open source projects can use the client libraries without being pulled into the GPL. If you're running MySQL on your own server behind your own application, none of this applies to you and the answer is free.
Version numbering changed recently and it catches people out when they go to pick a release. MySQL 9.7 was the last release line on sequential numbering. Releases after it use calendar versioning in YY.M.P format, so MySQL 26.7 is the July 2026 Innovation release. The split is documented in the release model documentation. LTS lines get five years of premier support plus three years of extended support under the Oracle Lifetime Support Policy and contain only necessary fixes, while an Innovation release is supported until the next Innovation release arrives. MySQL 9.7 and MySQL 8.4 are the current LTS lines. Pick an LTS line for a production server unless you have a reason to be on the front edge and the automated tests to survive it.
A related bit of trivia that comes up whenever version numbers do: MySQL never shipped a 6.0. It was in development for years, got abandoned around 2010 and the number was quietly skipped, which is why 5.7 was followed by 8.0. It has no bearing on anything you need to decide.
SQL Server: per-core licensing and the four-core floor
SQL Server Standard and Enterprise are per core and commercial. Microsoft requires at least four core licences per physical processor, and at least four per virtual machine when you licence individual VMs. Core licences come in packs of two.
A 2 vCPU instance therefore costs you a four-core licence. Downsizing the instance stops saving you money below four cores, and that changes how you plan capacity in a way MySQL has no equivalent for.
Free tier limits: MySQL Community and SQL Server Express
MySQL Community Edition has no size cap, no core cap and no memory cap. It's the whole server.
SQL Server Express is free for production too, and it's capped. Microsoft's editions and components page puts the maximum relational database size at 50 GB on SQL Server 2025, up from 10 GB on 2022 and earlier, with compute limited to the lesser of one socket or four cores and the buffer pool capped at 1,410 MB per instance. The memory limit is the one that decides things. A 40 GB database technically fits, and it will be reading from disk constantly, because Express holds 1.4 GB of pages in memory no matter how much RAM the server has. The rest of it, including why Developer edition is not a production option regardless of what the installer lets you do, sits in our guide to the SQL Server editions.
The two free tiers sit in different categories, then. MySQL's is the product. SQL Server's is a deliberately limited version of the product. For small applications Express is fine, and for anything with a growing dataset it's a countdown to a purchase order.
Storage engines: InnoDB and SQL Server's single engine
MySQL has a pluggable storage engine layer, which is the largest architectural difference between the two products. The SQL parser, the optimiser and the connection handling sit above an interchangeable engine that owns the actual storage. Oracle's storage engine documentation lists InnoDB as the default and general-purpose choice, with MyISAM, MEMORY, ARCHIVE and CSV alongside it plus NDB for clustering.
Only InnoDB and NDB support transactions. MyISAM was the default for years and there are still tables sitting on it, offering table-level locking, with no crash recovery and no way to roll back a failed statement. Check the engine per table on any MySQL database you inherit:
SELECT table_name, engine, table_rows
FROM information_schema.tables
WHERE table_schema = 'mydb';
InnoDB gives you ACID transactions, row-level locking, MVCC through undo logs, foreign key enforcement and crash recovery. Use it.
SQL Server has one engine and doesn't offer the choice. What it gives you instead are options inside that engine: rowstore or clustered columnstore for the same table, memory-optimised tables through In-Memory OLTP, filegroups and partitioning for physical layout. Different mechanism, similar outcome, with less chance of discovering that a critical table has been non-transactional since 2011.
MSSQL vs MySQL: the dialect differences that break a port
AUTO_INCREMENT and IDENTITY
MySQL marks a column AUTO_INCREMENT and you read the new key with LAST_INSERT_ID(). SQL Server uses the IDENTITY(1,1) property and you read it with SCOPE_IDENTITY() or an OUTPUT clause:
-- MySQL
CREATE TABLE orders (id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, customer_id INT);
INSERT INTO orders (customer_id) VALUES (42);
SELECT LAST_INSERT_ID();
-- SQL Server
CREATE TABLE dbo.Orders (Id INT IDENTITY(1,1) PRIMARY KEY, CustomerId INT);
INSERT INTO dbo.Orders (CustomerId) VALUES (42);
SELECT SCOPE_IDENTITY();
Both are session-scoped so concurrent inserts don't confuse them. Watch @@IDENTITY on the SQL Server side, which looks like the obvious choice and is the one older code uses: it returns the last identity generated in the session including one generated by a trigger on another table. Use SCOPE_IDENTITY().
LIMIT, TOP and OFFSET FETCH
MySQL paginates with LIMIT 10 OFFSET 20. SQL Server writes SELECT TOP (10) for the simple case, and for pagination it uses the ANSI clause:
SELECT * FROM dbo.Orders
ORDER BY CreatedAt DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
SQL Server requires an ORDER BY for that clause and MySQL doesn't require one for LIMIT, so a query that paginated fine in MySQL without an order can return the same row on two different pages after the port. Add the ORDER BY on both sides and the problem disappears.
Backticks and square brackets
MySQL quotes identifiers with backticks, SQL Server with square brackets, and the ANSI double quote works in both given the right setting. A schema dump full of `order` becomes [order], which is a search and replace until you hit a string literal containing a backtick. Then it's a search and replace with a bug in it.
String literals differ too. MySQL accepts double quotes around strings by default, so WHERE name = "Alice" works there and means something else entirely in SQL Server, where double quotes denote an identifier. That's one of the faster ways to get an "Invalid column name 'Alice'" error during a port.
Date and time handling
MySQL uses NOW(), CURDATE() and DATE_ADD(x, INTERVAL 7 DAY). SQL Server uses SYSDATETIME(), CAST(SYSDATETIME() AS date) and DATEADD(day, 7, x). The formatting functions have no overlap worth mentioning, with DATE_FORMAT() on one side and FORMAT() or CONVERT() with a style number on the other.
Our notes on CAST, CONVERT and T-SQL data types cover the style codes you'll need on the receiving end, which is most of the tedium in this part of a port. The one MySQL behaviour with no SQL Server equivalent at all is the zero date. '0000-00-00' is a value MySQL historically accepted in a DATE column and it converts to nothing. Find them first, decide what they meant and replace them with NULL or a sentinel date before you move any rows.
Collation and case sensitivity
The difference between MySQL and MS SQL Server that costs the most time during a port is collation, and it breaks applications without raising a single error.
MySQL's default character set is utf8mb4 and its default collation is utf8mb4_0900_ai_ci, which is accent-insensitive and case-insensitive. A default SQL Server install on an English locale uses SQL_Latin1_General_CP1_CI_AS, which is case-insensitive and accent-sensitive. Both ignore case. Only one ignores accents. Both names carry a CI, and I stopped reading there more than once before the difference landed.
So WHERE city = 'Malaga' matches a stored Málaga on MySQL and returns nothing on SQL Server. Search boxes, deduplication logic and unique constraints all behave differently, and the application never gets an exception to tell you about it. Test with accented data specifically, because English-language test fixtures will pass on both engines and prove nothing.
Identifier case is the second half of it. MySQL stores tables as files, so table and database names are case-sensitive on Unix and not on Windows, controlled by lower_case_table_names, which defaults to 0 on Linux, 1 on Windows and 2 on macOS. Code written on a Mac against Orders and deployed to Linux against orders fails at runtime. SQL Server's object names follow the database collation, which on a default install makes them case-insensitive everywhere, so this class of bug travels from MySQL to SQL Server and never the other way.

