Reference

PostgreSQL vs MySQL vs SQL Server vs SQLite: SQL Syntax Differences

10 min readUpdated September 10, 2026Every example verified
On this page 14 sections ▾
  1. Why dialects differ at all
  2. Limiting rows: LIMIT, TOP and FETCH FIRST
  3. Quoting: identifiers versus strings
  4. String concatenation
  5. Case sensitivity when comparing text
  6. Dates and times
  7. Booleans
  8. Auto-increment primary keys
  9. Upsert: insert or update in one statement
  10. Window functions
  11. NULL-safe comparison
  12. Which one should you learn first?
  13. Frequently asked questions
  14. Related reading

Standard SQL exists on paper. In practice every database speaks its own dialect, and most of the differences hide in exactly the places a tutorial skips: how you limit rows, how you glue strings together, how you ask for “today”. This page puts the four databases you are most likely to meet side by side, so a query that works in one can be translated to another in seconds. Nothing here is theoretical — each row is a mistake someone makes in their first week on a new system.

Why dialects differ at all

The SQL standard defines the core — SELECT, JOIN, GROUP BY, subqueries — and every major database implements that core faithfully. What the standard left unspecified for years (row limiting, string functions, date arithmetic, identity columns) each vendor filled in their own way, and those choices are now decades old and impossible to change without breaking customers. So the rule of thumb: the logic of a query travels; the trimmings do not.

How to read the examples

Blocks marked runs in the playground execute on this site’s in-browser engine as written. Blocks marked needs a full database are for your real PostgreSQL, MySQL, SQL Server or SQLite — the playground would not recognise that vendor-specific syntax, and pretending otherwise would teach you the wrong thing.

Limiting rows: LIMIT, TOP and FETCH FIRST

The single most common portability error. Three of the four use LIMIT; SQL Server does not.

Return the 5 most expensive products
DatabaseSyntax
PostgreSQLORDER BY unit_price DESC LIMIT 5 (also: FETCH FIRST 5 ROWS ONLY)
MySQLORDER BY unit_price DESC LIMIT 5
SQLiteORDER BY unit_price DESC LIMIT 5
SQL ServerSELECT TOP 5 ... or ORDER BY unit_price DESC OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY
SELECT name, unit_price
FROM Products
ORDER BY unit_price DESC
LIMIT 5;

SQL Server’s OFFSET ... FETCH form is the standard one and also works in PostgreSQL. It requires an ORDER BY, which is a good habit anyway: a limit without an order is a random sample.

-- SQL Server (and standard SQL)
SELECT name, unit_price
FROM Products
ORDER BY unit_price DESC
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;

Pagination follows the same split: LIMIT 10 OFFSET 20 in PostgreSQL, MySQL and SQLite; OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY in SQL Server.

Quoting: identifiers versus strings

Every database uses single quotes for string values. They disagree about how to quote a table or column name that clashes with a keyword or contains a space.

Quoting a column called "order"
DatabaseIdentifier quotesNote
PostgreSQL"order"Standard. Double-quoted names are case-sensitive; unquoted names fold to lower case.
MySQL`order`Backticks. Double quotes mean a string unless ANSI_QUOTES is on.
SQL Server[order]Square brackets. Double quotes also work when QUOTED_IDENTIFIER is on (the default).
SQLite"order"Standard double quotes; also accepts backticks and brackets for compatibility.

The trap

In MySQL, WHERE status = "shipped" is a string comparison and works. Paste the same line into PostgreSQL and it looks for a column named shipped. Use single quotes for values everywhere and you never hit this.

String concatenation

Joining first_name and last_name into one column is where the operators diverge most visibly.

Build a full name
DatabaseSyntax
PostgreSQLfirst_name || ' ' || last_name or CONCAT(first_name, ' ', last_name)
MySQLCONCAT(first_name, ' ', last_name) (|| means OR unless PIPES_AS_CONCAT is set)
SQL Serverfirst_name + ' ' + last_name or CONCAT(first_name, ' ', last_name)
SQLitefirst_name || ' ' || last_name
SELECT first_name || ' ' || last_name AS full_name, salary
FROM Employees
ORDER BY salary DESC
LIMIT 3;

CONCAT() is the most portable choice today — PostgreSQL 9.1+, MySQL, SQL Server 2012+ and SQLite 3.44+ all have it — and it has a second advantage: it treats NULL as an empty string, whereas || and + return NULL if any part is NULL.

Case sensitivity when comparing text

Does WHERE country = 'peru' match 'Peru'? It depends on the database, and the answer decides whether your filter silently drops rows.

Is text comparison case-sensitive by default?
DatabaseDefaultCase-insensitive match
PostgreSQLYesILIKE 'peru' or LOWER(country) = 'peru'
MySQLNo (depends on collation, usually *_ci)Already insensitive; use a *_cs or *_bin collation to force sensitivity
SQL ServerNo (depends on collation, usually CI)Already insensitive; COLLATE Latin1_General_CS_AS to force sensitivity
SQLiteYes for =, no for LIKE (ASCII only)LOWER(country) = 'peru'

The portable habit: compare LOWER(column) = LOWER(value) whenever user-typed input is involved. It works everywhere and makes the intent visible in the query.

Dates and times

Date handling is the biggest block of differences, because each vendor built its own function library. The good news is that the questions you ask are always the same four.

Today's timestamp
DatabaseFunction
PostgreSQLNOW() or CURRENT_TIMESTAMP
MySQLNOW() or CURRENT_TIMESTAMP
SQL ServerGETDATE() or SYSDATETIME()
SQLitedatetime('now')
Days between two dates (later minus earlier)
DatabaseExpression
PostgreSQLlater_date - earlier_date (dates subtract to an integer number of days)
MySQLDATEDIFF(later_date, earlier_date)
SQL ServerDATEDIFF(day, earlier_date, later_date) (note the reversed argument order)
SQLitejulianday(later_date) - julianday(earlier_date)
Truncate a date to the first of its month
DatabaseExpression
PostgreSQLDATE_TRUNC('month', order_date)
MySQLDATE_FORMAT(order_date, '%Y-%m-01')
SQL ServerDATEFROMPARTS(YEAR(order_date), MONTH(order_date), 1) (2022+: DATETRUNC(month, order_date))
SQLitestrftime('%Y-%m-01', order_date)
Add 30 days
DatabaseExpression
PostgreSQLorder_date + INTERVAL '30 days'
MySQLDATE_ADD(order_date, INTERVAL 30 DAY)
SQL ServerDATEADD(day, 30, order_date)
SQLitedate(order_date, '+30 days')

The argument-order trap

MySQL’s DATEDIFF(a, b) computes a − b. SQL Server’s DATEDIFF(day, a, b) computes b − a. Same name, opposite sign. If a “days since signup” report is coming out negative, this is why.

The playground on this site accepts YEAR(), MONTH() and SQL Server-style DATEDIFF(day, a, b) — see SQL Date Functions for runnable examples of every date pattern.

Booleans

How true and false are stored
DatabaseTypeLiterals
PostgreSQLBOOLEAN (a real type)TRUE / FALSE; WHERE active works on its own
MySQLBOOLEAN is an alias for TINYINT(1)TRUE / FALSE are 1 / 0
SQL ServerBIT1 / 0; WHERE active = 1 (a bare column is not allowed in WHERE)
SQLiteINTEGER1 / 0; TRUE / FALSE keywords since 3.23

Write WHERE active = 1 and it runs on all four. Write WHERE active and only PostgreSQL (and SQLite, loosely) accept it. Our sample database uses 1 and 0 for exactly this reason.

Auto-increment primary keys

A self-numbering id column
DatabaseDefinition
PostgreSQLid INT GENERATED ALWAYS AS IDENTITY (older: id SERIAL)
MySQLid INT AUTO_INCREMENT PRIMARY KEY
SQL Serverid INT IDENTITY(1,1) PRIMARY KEY
SQLiteid INTEGER PRIMARY KEY (AUTOINCREMENT is optional and rarely needed)

Getting the id you just inserted differs too: RETURNING id (PostgreSQL, SQLite 3.35+), LAST_INSERT_ID() (MySQL), SCOPE_IDENTITY() (SQL Server).

Upsert: insert or update in one statement

“Insert this row, or update it if the key already exists” is one of the least portable statements in SQL.

-- PostgreSQL and SQLite 3.24+
INSERT INTO Products (product_id, name, category, unit_price, in_stock)
VALUES (201, 'Aurora Laptop 14"', 'Hardware', 1249, 40)
ON CONFLICT (product_id) DO UPDATE
  SET unit_price = EXCLUDED.unit_price, in_stock = EXCLUDED.in_stock;
-- MySQL
INSERT INTO Products (product_id, name, category, unit_price, in_stock)
VALUES (201, 'Aurora Laptop 14"', 'Hardware', 1249, 40)
ON DUPLICATE KEY UPDATE
  unit_price = VALUES(unit_price), in_stock = VALUES(in_stock);
-- SQL Server
MERGE Products AS target
USING (SELECT 201 AS product_id, 1249 AS unit_price, 40 AS in_stock) AS src
  ON target.product_id = src.product_id
WHEN MATCHED THEN UPDATE SET unit_price = src.unit_price, in_stock = src.in_stock
WHEN NOT MATCHED THEN INSERT (product_id, name, category, unit_price, in_stock)
  VALUES (201, 'Aurora Laptop 14"', 'Hardware', 1249, 40);

Window functions

ROW_NUMBER, RANK and friends are standard SQL, but support arrived at very different times, and older servers are still common in companies.

When window functions became available
DatabaseSince
PostgreSQL8.4 (2009) — full support for years
MySQL8.0 (2018) — MySQL 5.7 has none; MariaDB 10.2+
SQL Server2005 for ranking functions; 2012 for frames (ROWS BETWEEN, LAG/LEAD)
SQLite3.25 (2018)

If you are on MySQL 5.7 and need a rank, the classic workaround is a correlated subquery counting rows with a higher value. The window functions guide covers the modern syntax and marks which examples run in this site’s engine.

NULL-safe comparison

NULL = NULL is not true in any database — it is unknown. When you genuinely want “both NULL counts as equal”, each vendor has its own spelling:

Treat two NULLs as equal
DatabaseOperator
PostgreSQLa IS NOT DISTINCT FROM b
MySQLa <=> b
SQL Servera IS NOT DISTINCT FROM b (2022+; otherwise COALESCE tricks)
SQLitea IS b

Which one should you learn first?

It matters less than beginners fear. Ninety percent of what you learn transfers unchanged; this page is the other ten percent. Practical advice:

  • Learning on your own: PostgreSQL. It follows the standard most closely, so habits formed there port cleanly, and it is free everywhere.
  • Joining a team: whatever the team runs. Ask on day one, then read the matching column of this page.
  • Data analyst roles: expect PostgreSQL, MySQL or a warehouse (BigQuery, Snowflake, Redshift) — all close to PostgreSQL in feel.
  • Enterprise / .NET shops: SQL Server. Learn TOP, GETDATE and bracket quoting early.
  • Mobile or embedded: SQLite, which is deliberately small but surprisingly standard.

Everything on this site is written to stay dialect-neutral where possible, and to say so plainly where it is not. Start with the beginner’s guide and the syntax you learn will work almost everywhere.

Frequently asked questions

Is SQL the same in every database?

The core is the same: SELECT, WHERE, JOIN, GROUP BY, ORDER BY and subqueries work identically in PostgreSQL, MySQL, SQL Server and SQLite. The differences live in row limiting, string and date functions, quoting rules, identity columns and upserts. Learn the core once, then keep a translation table like this one for the rest.

Which SQL dialect is closest to the standard?

PostgreSQL follows the ANSI/ISO standard most closely, which is why habits learned there transfer well. SQL Server is close but uses TOP and a different function library. MySQL and SQLite are pragmatic and permissive, accepting several spellings for the same thing.

Why does LIMIT not work in SQL Server?

SQL Server never adopted the LIMIT keyword. Use SELECT TOP n for a simple cap, or the standard OFFSET n ROWS FETCH NEXT m ROWS ONLY form after an ORDER BY, which also works in PostgreSQL.

Do SQL interview questions depend on the dialect?

Rarely. Interviewers ask about joins, aggregation, subqueries, window functions and NULL handling, all of which are standard. Most will accept any dialect as long as you say which one you are writing. If you are unsure, write standard SQL and mention the vendor-specific alternative.

Which dialect does the playground on this site use?

The in-browser engine (AlaSQL) accepts LIMIT, both || and CONCAT() for strings, and YEAR(), MONTH() and DATEDIFF(day, a, b) for dates, so it behaves like a mix of MySQL and SQL Server for the functions covered here. Examples that need vendor-specific syntax are marked as needing a full database rather than presented as runnable.

About the author

SQL Practice

SQL Practice publishes free SQL tutorials, an in-browser playground and 42 practice exercises, built so you can run what you read. Every runnable example in our tutorials is executed against the site’s sample database before it is published. Read how we keep tutorials accurate, or report a mistake.