Common Gotchas
The last stretch isn't new syntax to memorize — it's a handful of behavioral differences that don't show up until something works fine in development and then breaks (or, worse, silently returns wrong data) somewhere else: a different OS, a different table engine, a stricter server mode. These are the ones that cost the most debugging time precisely because nothing throws an error.
💡 The Bottom Line
SQL Server table and column names are case-insensitive by default, everywhere. MySQL's
case-sensitivity for table names depends on the underlying filesystem (case-sensitive on Linux,
case-insensitive on Windows and macOS by default) — the exact same script can behave differently
depending only on what OS the MySQL server happens to run on. On top of that, MySQL's default
storage engine (InnoDB) supports transactions much like SQL Server does, but older tables or a
misconfigured engine choice (MyISAM) silently drop transactional guarantees, and MySQL has
historically been more lenient than SQL Server about non-aggregated columns in a
GROUP BY query. Treat all of these as "runs fine until it doesn't."
Case sensitivity of table names
SELECT * FROM Orders;
SELECT * FROM orders;
SELECT * FROM ORDERS;
-- all three work identically
SELECT * FROM Orders; -- works
SELECT * FROM orders; -- works only if the
-- table was created
-- as "orders"
SELECT * FROM ORDERS; -- Error 1146:
-- table doesn't exist
📋 Key differences
- SQL Server's default collation makes table, column, and database names case-insensitive for matching purposes, regardless of platform.
- MySQL delegates table-name case sensitivity to the filesystem it's running on: Linux filesystems are typically case-sensitive, so MySQL table names are case-sensitive there; Windows and (by default) macOS filesystems are case-insensitive, so the same MySQL install behaves more like SQL Server on those platforms.
- Column names, unlike table names, are always case-insensitive in MySQL, regardless of OS — the inconsistency is specifically about tables (and databases), not columns.
⚠️ Gotcha
A team develops on macOS or Windows, where Orders and orders are the same
table, then deploys to a Linux production server, where they're two different (and one
nonexistent) tables. The failure only appears in production, on whichever queries happen to use
the "wrong" casing — and it's not a MySQL bug, it's the OS filesystem underneath it. The fix is
discipline, not configuration: pick one casing convention (snake_case, all lowercase, is the
overwhelming MySQL norm) and never deviate, so the filesystem's behavior never matters.
Transactions and storage engines
SQL Server's storage layer supports transactions as a fundamental, non-optional guarantee — you
don't choose whether a table participates in transactions. MySQL's transactional support is a
property of the storage engine a table uses, and MySQL supports more than one. The
default engine on modern MySQL is InnoDB, which does support full ACID
transactions, row-level locking, and foreign keys — behaviorally close to what a T-SQL developer
already expects. The older MyISAM engine does not support transactions at all: a
ROLLBACK on a MyISAM table simply does nothing, silently.
📋 Key differences
- Check a table's engine with
SHOW TABLE STATUS LIKE 'orders';orSHOW CREATE TABLE orders;— theENGINE=clause tells you immediately. - InnoDB is the default for new tables on any current MySQL version, so a fresh install
behaves transactionally out of the box — the risk is almost entirely with older databases or
an explicit
ENGINE=MyISAMleft over from a much older schema. - Foreign key constraints are also engine-dependent: MyISAM silently ignores
FOREIGN KEYclauses in aCREATE TABLEstatement (they parse but enforce nothing) — another reason a stray MyISAM table can look correct in the schema while enforcing none of its declared constraints.
⚠️ Gotcha
Wrapping statements in START TRANSACTION ... COMMIT/ROLLBACK against a MyISAM table
doesn't raise an error — it just doesn't do anything. A ROLLBACK after a failed
operation leaves every already-executed statement's changes in place, exactly as if no
transaction had been used at all. Always confirm ENGINE=InnoDB on any table you expect
transactional behavior from, especially on databases migrated from an older MySQL version.
GROUP BY strictness
SELECT CustomerId, OrderDate, COUNT(*)
FROM Orders
GROUP BY CustomerId;
-- Error: OrderDate is invalid in the
-- select list because it's not
-- contained in an aggregate function
-- or the GROUP BY clause
SELECT customer_id, order_date, COUNT(*)
FROM orders
GROUP BY customer_id;
-- historically: runs, picks an
-- arbitrary order_date value per group
SQL Server has always enforced the ANSI rule strictly: every non-aggregated column in the
select list must appear in the GROUP BY clause. Older MySQL versions were famously
lenient — they'd run the query anyway and pick an arbitrary (essentially unpredictable) row's
value for the non-grouped column. Modern MySQL (5.7.5+) enables
ONLY_FULL_GROUP_BY in the default SQL mode, which brings its behavior in line with SQL
Server's stricter rule — but the setting can be turned off, and plenty of production servers
(or their configuration inherited from an older version) still run without it.
⚠️ Gotcha
Never rely on ONLY_FULL_GROUP_BY being on just because it's the modern default —
check the server's actual sql_mode with SELECT @@sql_mode; before assuming a
loose GROUP BY will be caught. A query that "worked" against a lenient server can start
throwing errors the moment it's pointed at a strictly-configured one, or vice versa — silently
return different arbitrary values per run against a lenient one. Either way, write
GROUP BY queries as if the strict rule always applies: include every non-aggregated
select-list column in the GROUP BY clause, on both engines.
Date/time function names
SELECT GETDATE();
SELECT DATEADD(DAY, 7, GETDATE());
SELECT DATEDIFF(DAY, StartDate, EndDate);
SELECT NOW();
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY);
SELECT DATEDIFF(end_date, start_date);
📋 Key differences
GETDATE()becomesNOW()(MySQL also hasCURDATE()andCURTIME()for just the date or time portion).DATEADD(unit, number, date)becomesDATE_ADD(date, INTERVAL number unit)— note the argument order flips, and the unit/number pairing becomes a singleINTERVALexpression.- MySQL's
DATEDIFF(a, b)always returns whole days (a - b) and takes no unit argument; SQL Server'sDATEDIFF(unit, a, b)takes an explicit unit and computesb - a— both the argument order and the meaning of the result are reversed, so this one is worth double-checking every time rather than trusting muscle memory.
🔧 Try It Yourself
On a MySQL instance, run SELECT @@sql_mode; and check whether
ONLY_FULL_GROUP_BY appears in the result. Then run
SHOW CREATE TABLE against a table you use often and confirm its
ENGINE= is InnoDB. Finally, write one query using NOW() and
DATE_ADD(..., INTERVAL 1 DAY), translating from a T-SQL query you'd normally write with
GETDATE() and DATEADD.
🙋 There Are No Dumb Questions
Q: If my MySQL server is on Linux, is there any way to make table names
case-insensitive without changing every query?
A: Yes — the server-level setting lower_case_table_names controls this, but it can
only be set at server initialization (changing it on an existing data directory isn't safely
supported), and it doesn't fix the underlying issue if you ever move the database to a different
OS or a managed cloud provider with a fixed setting. The far more portable fix is simply to
standardize on one casing (lowercase snake_case is the MySQL convention) in every script and
migration, so the setting never has to matter.
✅ Quick Check
A query with SELECT customer_id, order_date, COUNT(*) FROM orders GROUP BY customer_id;
runs without error on one MySQL server but throws an error on another. What's the most likely
explanation?
That's the last lesson — time for the recap, where all five lessons come together into one quick-reference summary.