PostgreSQL, MySQL, SQL Server, Oracle and SQLite Compared

One reference table for the differences that actually break code: paging, auto increment, strings, dates, upserts and the features each product lacks.

Concept

SQL is a standard that every product implements partially and extends privately. The core - SELECT, JOIN, GROUP BY, constraints, transactions - is genuinely portable. Everything around the edges is not.

The differences that break code

Limiting rows

ProductSyntax
MySQL, MariaDB, PostgreSQL, SQLiteLIMIT 20 OFFSET 40
Standard, PostgreSQL, SQL Server 2012+, Oracle 12c+OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY
SQL Server (simple top N)SELECT TOP 20 ...

Auto generated keys

ProductDeclarationLast inserted id
MySQL / MariaDBAUTO_INCREMENTLAST_INSERT_ID()
PostgreSQLGENERATED ALWAYS AS IDENTITY or SERIALRETURNING id
SQL ServerIDENTITY(1,1)SCOPE_IDENTITY() or OUTPUT
OracleGENERATED AS IDENTITYRETURNING ... INTO
SQLiteINTEGER PRIMARY KEY AUTOINCREMENTlast_insert_rowid()

Strings

-- Concatenation
a || b                          -- standard, PostgreSQL, Oracle, SQLite
CONCAT(a, b)                    -- MySQL, MariaDB (|| means OR by default)
a + b                           -- SQL Server

-- Length
CHAR_LENGTH(s)                  -- standard, MySQL, PostgreSQL
LEN(s)                          -- SQL Server
LENGTH(s)                       -- Oracle, SQLite (bytes in MySQL!)

-- Case sensitivity of =
-- MySQL:      collation driven, usually case INsensitive
-- PostgreSQL: case SENSITIVE (ILIKE exists for patterns)
-- SQL Server: collation driven, usually case insensitive
-- Oracle:     case sensitive

Dates

-- Now
NOW()          -- MySQL, PostgreSQL      GETDATE()   -- SQL Server
CURRENT_DATE   -- standard, most         SYSDATE     -- Oracle

-- Add 7 days
DATE_ADD(d, INTERVAL 7 DAY)     -- MySQL
d + INTERVAL '7 days'           -- PostgreSQL
DATEADD(DAY, 7, d)              -- SQL Server
d + 7                           -- Oracle
DATE(d, '+7 days')              -- SQLite

-- Difference in days
DATEDIFF(a, b)                  -- MySQL: a minus b
DATEDIFF(DAY, b, a)             -- SQL Server: note the reversed argument order
a - b                           -- PostgreSQL, Oracle

Upsert

INSERT ... ON DUPLICATE KEY UPDATE ...    -- MySQL, MariaDB
INSERT ... ON CONFLICT (col) DO UPDATE    -- PostgreSQL, SQLite
MERGE INTO ... WHEN MATCHED THEN ...      -- standard, SQL Server, Oracle

NULL handling

COALESCE(a, b)   -- standard, everywhere. Prefer this.
IFNULL(a, b)     -- MySQL, SQLite
ISNULL(a, b)     -- SQL Server
NVL(a, b)        -- Oracle

Feature availability

FeatureMySQLMariaDBPostgreSQLSQL ServerOracleSQLite
CTEs8.0+10.2+YesYesYes3.8.3+
Window functions8.0+10.2+Yes2012+Yes3.25+
FULL OUTER JOINNoNoYesYesYes3.39+
INTERSECT / EXCEPT8.0.31+10.3+YesYesMINUSYes
Materialised viewsNoNoYesIndexed viewsYesNo
Partial / filtered indexNoNoYesYesFunction basedYes
LATERAL / APPLY8.0.14+NoYesAPPLY12c+No
Stored proceduresYesYes11+YesYesNo
Row level securityNoNoYesYesYesNo
Array / JSON typesJSONJSONBoth, richlyJSONJSONJSON1 ext

Behavioural differences, not just syntax

BehaviourDifference
Default isolationMySQL REPEATABLE READ; PostgreSQL, SQL Server and Oracle READ COMMITTED
Transactional DDLPostgreSQL and SQL Server yes; MySQL and Oracle auto commit
NULL orderingMySQL and SQLite first ascending; PostgreSQL and Oracle last
Empty stringOracle treats '' as NULL; everyone else does not
Identifier casePostgreSQL folds to lower; Oracle folds to upper; MySQL depends on the file system
GROUP BY strictnessMySQL 8 enforces ONLY_FULL_GROUP_BY by default; MariaDB does not
Integer divisionPostgreSQL, SQL Server and Oracle truncate; MySQL returns a decimal

Important rules

  • The core query language is portable. Paging, identity, dates, strings and upserts are not.
  • Version matters as much as product: MySQL 5.7 and 8.0 differ more than some separate products do.
  • Behavioural differences are more dangerous than syntax ones - syntax errors are loud, behaviour changes are silent.
  • Oracle's empty string equals NULL rule breaks assumptions carried from any other product.

Common mistakes

  • Copying an answer from the internet without checking which product it targets.
  • Porting an application between products and not re checking the isolation level.
  • Using LIMIT in code destined for SQL Server or older Oracle.
  • Assuming || concatenates on MySQL - it is OR.
  • Relying on MySQL's loose GROUP BY, then moving to PostgreSQL.

Best practices

  • Write down the target product and version in the project documentation.
  • Prefer standard forms - COALESCE, CAST, EXTRACT, CHAR_LENGTH - where one exists.
  • Isolate the non portable parts in one layer, so a port touches few files.
  • Run tests against the same product and version as production.

Practice

  1. Write "the 20 most recent orders, skipping the first 40" for MySQL, PostgreSQL and SQL Server.
  2. Write an upsert into customers for MySQL and for PostgreSQL.
  3. List three behavioural - not syntactic - differences that would silently change results in a port from PostgreSQL to MySQL.

Useful resources

Hand picked references for this topic
Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All SQL notes →
SQL

Writing Portable SQL

How much portability is worth paying for, which constructs are safe everywhere, and how to isolate the parts that can never be portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.