ANSI SQL Standards & Database Differences: Mistakes
Module: Foundational Concepts
SELECT first_name || ' ' || last_name FROM users; -- In MySQL
SELECT CONCAT(first_name, ' ', last_name) FROM users;
MySQL treats || as logical OR by default (returning 0 or 1). It requires CONCAT() for string concatenation. PostgreSQL and Oracle support || as the ANSI concatenation operator. SQL Server uses + instead. CONCAT() is the only function that works on all four.
Always use CONCAT() for string concatenation in any code that might run on MySQL.
High
In MySQL, || is logical OR. This returns 0, not the concatenated string.
SELECT * FROM orders ORDER BY created_at LIMIT 10; -- In SQL Server
SELECT * FROM orders ORDER BY created_at OFFSET 0 ROWS FETCH NEXT 10 ROWS ONLY;
LIMIT is MySQL and PostgreSQL syntax. SQL Server uses TOP (no offset) or OFFSET/FETCH NEXT. Note that SQL Server uses FETCH NEXT while ANSI SQL uses FETCH FIRST — a subtle difference.
Use FETCH FIRST/NEXT for portability on modern databases. Use an ORM for true cross-database pagination.
High
Incorrect syntax near 'LIMIT'. SQL Server does not support LIMIT.
SELECT * FROM users WHERE email = NULL;
SELECT * FROM users WHERE email IS NULL;
NULL comparisons using = always return NULL (unknown), which is treated as false in WHERE clauses. This is SQL-92 standard behavior on every database. Always use IS NULL or IS NOT NULL. The same applies to != NULL — use IS NOT NULL instead.
If you ever get a WHERE clause returning unexpected empty results, check for = NULL comparisons.
Critical
Returns 0 rows on every database. NULL = NULL evaluates to NULL, not TRUE.
user_id INT AUTO_INCREMENT -- Schema targeting PostgreSQL
user_id BIGINT GENERATED ALWAYS AS IDENTITY -- or use BIGSERIAL
AUTO_INCREMENT is MySQL-only. PostgreSQL uses SERIAL/BIGSERIAL (legacy) or GENERATED AS IDENTITY (SQL:2003 standard, preferred since PostgreSQL 10). SQL Server uses IDENTITY(1,1). Oracle 12c+ uses GENERATED AS IDENTITY.
Use GENERATED AS IDENTITY for new schemas on PostgreSQL 10+, SQL Server 2012+, and Oracle 12c+. Fall back to SERIAL for older PostgreSQL.
High
syntax error at or near 'AUTO_INCREMENT'
REPLACE INTO user_profiles (email, name) VALUES (?, ?);
INSERT INTO user_profiles (email, name) VALUES (?, ?) ON DUPLICATE KEY UPDATE name = VALUES(name);
REPLACE INTO is not an upsert. It DELETEs the conflicting row and INSERTs a new one. This fires DELETE triggers, cascades to foreign key children (potentially deleting them), advances AUTO_INCREMENT sequences, and can cause race conditions. Use ON DUPLICATE KEY UPDATE in MySQL or ON CONFLICT in PostgreSQL.
Treat REPLACE INTO as deprecated. It is never the right tool for upsert semantics.
Critical
REPLACE INTO silently deletes and reinserts rows, cascading DELETE to child tables.
SELECT * FROM feature_flags WHERE is_enabled = TRUE; -- In SQL Server
SELECT * FROM feature_flags WHERE is_enabled = 1;
SQL Server uses the BIT data type (values 1 and 0) with no BOOLEAN type and no TRUE/FALSE literals. PostgreSQL has a real BOOLEAN type with TRUE/FALSE. MySQL has BOOLEAN as an alias for TINYINT(1) and accepts TRUE/FALSE as 1/0. Always use 1/0 for boolean columns in cross-database schemas.
Use 1/0 for boolean values in any SQL that might run on SQL Server.
Medium
Must declare the scalar variable '@TRUE' or similar SQL Server parsing error.