Which parts of SQL are portable across engines, and which parts break when the same query moves to another database?
The portable core is larger than most people assume and the dialect-specific parts are fewer than the folklore suggests. Standard SELECT, JOIN, GROUP BY, HAVING, subqueries, CTEs, window functions, CASE and the standard aggregate functions all behave identically on MySQL, PostgreSQL, SQLite and SQL Server. If your query is only that, you can move it without edits.
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name VARCHAR(120) NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
amount DECIMAL(10, 2) NOT NULL
);
INSERT INTO customers (id, name) VALUES (1, 'Ada Lovelace'), (2, 'Grace Hopper');
INSERT INTO orders (id, customer_id, amount) VALUES
(1, 1, 120.00), (2, 1, 340.50), (3, 2, 75.25);
WITH revenue AS (
SELECT customer_id, SUM(amount) AS total
FROM orders
GROUP BY customer_id
)
SELECT c.name,
r.total,
CASE WHEN r.total > 200 THEN 'gold' ELSE 'standard' END AS tier
FROM revenue AS r
JOIN customers AS c ON c.id = r.customer_id
ORDER BY r.total DESC;Where dialects diverge is in four areas. Identifiers: MySQL quotes with backticks by default, standard SQL with double quotes. Upserts: the standard ON CONFLICT, MySQL's ON DUPLICATE KEY UPDATE, SQL Server's MERGE. Row limiting: LIMIT n OFFSET m everywhere except SQL Server, which wants OFFSET m ROWS FETCH NEXT n ROWS ONLY and requires an ORDER BY. Functions: CONCAT, NOW and ILIKE are not standard, so the portable spellings are ||, datetime('now') and plain LIKE.
Type strictness is the subtlest difference. PostgreSQL refuses to compare a text column to an integer; MySQL coerces silently. The same query can pass review on MySQL and fail in production on PostgreSQL, which is why portability testing matters more than portability theory.