MySQL vs PostgreSQL vs SQLite
SQL is a standard, but every database speaks its own dialect: the standard core plus its own extras and quirks. Most of what you have learned in this course works everywhere. The rest, such as how to limit rows, create automatic IDs or join text, changes from one database to the next.
You will meet more than one database in your career. Django and Python projects often start on SQLite, many startups choose PostgreSQL, banks and large companies often run Microsoft SQL Server, and the browser's Try-it editor in this course is SQLite. This lesson compares the four on the points that trip people up most, and ends with how the SQL standard grew from the 1970s to today.
Four databases at a glance
| MySQL | PostgreSQL | SQLite | SQL Server | |
|---|---|---|---|---|
| Kind | Server | Server | A library inside your app, one file per database | Server |
| Owner and licence | Oracle; free Community edition (GPL) | Community project; permissive open-source licence | Public domain | Microsoft; commercial, free Express edition |
| Typical use | Websites, PHP, WordPress | Apps that need strict types, advanced SQL, GIS | Mobile apps, desktop apps, tests, small sites | Company systems built on .NET and Windows |
| Identifier quotes | `name` | "name" | "name" (also accepts backticks) | [name] or "name" |
Server databases run as a separate program that many users connect to over a network. SQLite has no server at all: your program reads and writes the database file directly, which makes it tiny and easy, but less suited to many people writing at once.
Limiting rows
-- MySQL, PostgreSQL, SQLite
SELECT product_name, price FROM products ORDER BY price DESC LIMIT 3 OFFSET 2;
-- SQL standard: PostgreSQL, and SQL Server 2012 or later
SELECT product_name, price FROM products ORDER BY price DESC
OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY;
-- SQL Server: first rows only
SELECT TOP 3 product_name, price FROM products ORDER BY price DESC;LIMIT is not in the SQL standard, but it is so popular that three of the four support it. MySQL does not accept the standard FETCH form, and SQL Server does not accept LIMIT. MySQL also allows the short form LIMIT 2, 3 (skip 2, take 3), which you will see in older PHP code. The first form runs in the browser:
SELECT product_id, product_name, price
FROM products
ORDER BY price DESC
LIMIT 3 OFFSET 2;product_id | product_name | price -----------+---------------------+------ 9 | 1TB External SSD | 6499 12 | Graphics Tablet | 4599 2 | Mechanical Keyboard | 3499 (3 rows)
Automatic IDs
Each database has its own way to number new rows:
-- MySQL
CREATE TABLE reviews (review_id INT AUTO_INCREMENT PRIMARY KEY, rating INT NOT NULL);
-- PostgreSQL 10 or later (standard SQL); older code uses: review_id SERIAL PRIMARY KEY
CREATE TABLE reviews (review_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, rating INT NOT NULL);
-- SQLite: an INTEGER PRIMARY KEY is numbered automatically
CREATE TABLE reviews (review_id INTEGER PRIMARY KEY, rating INTEGER NOT NULL);
-- SQL Server: start at 1, add 1 each time
CREATE TABLE reviews (review_id INT IDENTITY(1,1) PRIMARY KEY, rating INT NOT NULL);GENERATED ... AS IDENTITY is the form from the SQL standard and is preferred over SERIAL in new PostgreSQL code. In SQLite, an INTEGER PRIMARY KEY column becomes the table's internal row number; the extra AUTOINCREMENT keyword only stops old numbers from being reused and is rarely needed.
Text, quotes and comparisons
Joining text is the classic difference:
| Task | MySQL | PostgreSQL | SQLite | SQL Server |
|---|---|---|---|---|
| Join two strings | CONCAT(a, b) | a || b or CONCAT(a, b) | a || b or concat(a, b) | a + b or CONCAT(a, b) |
Meaning of || | Logical OR | Join text | Join text | Not an operator |
| Join many rows into one string | GROUP_CONCAT() | STRING_AGG() | group_concat() | STRING_AGG() |
| Text length in characters | CHAR_LENGTH() | LENGTH() | length() | LEN() |
'abc' = 'ABC' | True (default collation) | False | False | Depends on collation, usually true |
Watch the NULL rules too. MySQL's CONCAT() returns NULL if any argument is NULL, while SQL Server's and SQLite's concat() treat NULL as empty text. In PostgreSQL, || with a NULL gives NULL but CONCAT() skips NULLs. COALESCE() is standard and behaves the same everywhere.
PostgreSQL has one more trap: it turns unquoted names into lower case. CREATE TABLE "Customers" with quotes creates a name that you must then always quote with the same capitals. Stick to lower-case names without quotes and the problem never arises in any database.
Booleans and data types
MySQL has a BOOLEAN type, but it is really TINYINT(1), and TRUE is just 1. SQLite has no boolean type either and stores 0 and 1. PostgreSQL has a true boolean type, and its psql client shows results as t and f. SQL Server uses a BIT column holding 0 or 1. So a comparison in the column list prints 1 and 0 in MySQL and the browser:
SELECT product_name, stock,
stock > 0 AS in_stock,
stock BETWEEN 1 AND 20 AS low_stock
FROM products
WHERE category = 'Accessories'
ORDER BY stock;product_name | stock | in_stock | low_stock --------------------+-------+----------+---------- HD Webcam | 0 | 0 | 0 Graphics Tablet | 12 | 1 | 1 Mechanical Keyboard | 45 | 1 | 0 USB-C Hub | 60 | 1 | 0 Laptop Stand | 75 | 1 | 0 Wireless Mouse | 120 | 1 | 0 (6 rows)
Data types differ more deeply:
- SQLite uses dynamic typing. A column's declared type is only a preference, called its type affinity, and SQLite will store the text
'abc'in anINTEGERcolumn. Since version 3.37 you can declare a tableSTRICTto make it reject wrong types. SQLite also has no date type: dates are stored as text such as'2025-04-18'and handled with functions likedate()andstrftime(). - PostgreSQL is the strictest and the richest: it refuses to compare a number with text without an explicit cast, and adds types such as
UUID, arrays,JSONBandTIMESTAMPTZ. - SQL Server uses
NVARCHARfor Unicode text (Hindi, the ₹ sign) andDATETIME2for dates with times. - MySQL sits in between: it converts types for you where it can, which is convenient but can hide mistakes.
Insert or update: upserts
"Insert this row, or update it if the key already exists" is called an upsert, and every database spells it differently. Here the shop adds 50 pen drives to stock:
-- MySQL 8.0.19 or later
INSERT INTO products (product_id, product_name, category, price, stock)
VALUES (10, '64GB Pen Drive', 'Storage', 549.00, 50) AS new
ON DUPLICATE KEY UPDATE stock = products.stock + new.stock;
-- PostgreSQL 9.5 or later, and SQLite 3.24 or later
INSERT INTO products (product_id, product_name, category, price, stock)
VALUES (10, '64GB Pen Drive', 'Storage', 549.00, 50)
ON CONFLICT (product_id) DO UPDATE SET stock = products.stock + excluded.stock;
-- SQL Server (PostgreSQL 15 or later also has MERGE)
MERGE products AS target
USING (VALUES (10, '64GB Pen Drive', 'Storage', 549.00, 50))
AS src (product_id, product_name, category, price, stock)
ON target.product_id = src.product_id
WHEN MATCHED THEN UPDATE SET stock = target.stock + src.stock
WHEN NOT MATCHED THEN INSERT (product_id, product_name, category, price, stock)
VALUES (src.product_id, src.product_name, src.category, src.price, src.stock);The ON CONFLICT version runs in the browser, but not in MySQL, so it is not a Try-it example here. MERGE is the form in the SQL standard.
Other everyday differences
| Feature | MySQL | PostgreSQL | SQLite | SQL Server |
|---|---|---|---|---|
7 / 2 | 3.5000 | 3 | 3 | 3 |
| Current date and time | NOW() | NOW() | datetime('now') | GETDATE() |
| Replace NULL (two values) | IFNULL(a, b) | COALESCE(a, b) | IFNULL(a, b) | ISNULL(a, b) |
FULL OUTER JOIN | Not supported | Yes | Yes (3.39 or later) | Yes |
INTERSECT / EXCEPT | Yes (8.0.31 or later) | Yes | Yes | Yes |
CURRENT_TIMESTAMP and COALESCE() are standard and work in all four, so prefer them in code that must move between databases. The safest habit of all: develop and test on the same database you use in production. An app built on SQLite and deployed on MySQL will meet the differences above on the day it goes live.
Most everyday SQL is still identical. This query uses only standard features (LEFT JOIN, GROUP BY, CASE, COUNT), so it runs unchanged in all four databases:
SELECT c.city,
COUNT(o.order_id) AS orders,
CASE
WHEN COUNT(o.order_id) >= 2 THEN 'repeat'
WHEN COUNT(o.order_id) = 1 THEN 'one-time'
ELSE 'none yet'
END AS buyer_type
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.customer_id
WHERE c.state IN ('Haryana', 'Maharashtra')
GROUP BY c.city
ORDER BY orders DESC, c.city;city | orders | buyer_type ---------+--------+----------- Rohtak | 4 | repeat Mumbai | 2 | repeat Gurugram | 1 | one-time Pune | 0 | none yet (4 rows)
A short history of the SQL standard
SQL began at IBM. In 1970 Edgar F. Codd published the relational model, the idea of storing data in tables linked by values. In the mid-1970s, Donald Chamberlin and Raymond Boyce designed a query language for it called SEQUEL, later renamed SQL because of a trademark. In 1979 the company that became Oracle released the first commercial SQL database. ANSI published the first SQL standard in 1986, and ISO adopted it in 1987.
| Version | Notable additions |
|---|---|
| SQL-86, SQL-89 | The first standard: SELECT, INSERT, UPDATE, DELETE, basic constraints |
| SQL-92 | JOIN ... ON syntax, CASE, CAST, date and time types |
| SQL:1999 | Recursive WITH queries, triggers, the BOOLEAN type, ROLLUP and CUBE |
| SQL:2003 | Window functions, MERGE, identity columns, sequences, XML |
| SQL:2008 | FETCH FIRST, TRUNCATE TABLE |
| SQL:2011 | Temporal tables that keep the history of every row |
| SQL:2016 | JSON functions, row pattern matching |
| SQL:2023 | Property graph queries (SQL/PGQ), a JSON data type, GREATEST, LEAST, ANY_VALUE |
No database implements the whole standard, and every vendor adds features before they are standardised; LIMIT and AUTO_INCREMENT are examples. Window functions are part of the 2003 standard, yet MySQL only added them in version 8.0 in 2018.
Key points
- Every database speaks a dialect: standard SQL plus its own extras.
- Row limits:
LIMITin MySQL, PostgreSQL and SQLite;TOPorOFFSET ... FETCHin SQL Server. - Auto IDs:
AUTO_INCREMENT,GENERATED AS IDENTITY(orSERIAL),INTEGER PRIMARY KEY,IDENTITY(1,1). ||joins text in PostgreSQL and SQLite but means OR in MySQL; useCONCAT()in MySQL.- Upserts:
ON DUPLICATE KEY UPDATE(MySQL),ON CONFLICT(PostgreSQL, SQLite),MERGE(SQL Server, standard). - SQLite has dynamic types and no date type; PostgreSQL is strict; MySQL converts types quietly.
- The standard grew from SQL-86 to SQL:2023; prefer standard features such as
COALESCE,CASEandJOIN ... ONfor portable code.
Exercise
A MySQL report shows the three most recent orders with the customer's full name: SELECT o.order_id, CONCAT(c.first_name, ' ', c.last_name) AS customer FROM orders o JOIN customers c ON c.customer_id = o.customer_id ORDER BY o.order_date DESC LIMIT 3; Rewrite it for PostgreSQL using ||, and for SQL Server using TOP and +.
Show solution
-- PostgreSQL
SELECT o.order_id, c.first_name || ' ' || c.last_name AS customer
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC
LIMIT 3;
-- SQL Server
SELECT TOP 3 o.order_id, c.first_name + ' ' + c.last_name AS customer
FROM orders o
JOIN customers c ON c.customer_id = o.customer_id
ORDER BY o.order_date DESC;Only the text joining and the row limit change; the join, the sorting and the aliases are standard SQL. The PostgreSQL version also runs in the browser, because SQLite uses || and LIMIT too, and it returns orders 115, 114 and 113.
Quick Quiz
1. Which statement returns the first 5 rows in SQL Server?
SQL Server uses TOP, or the standard OFFSET ... FETCH; it does not support LIMIT.
2. What does 'Aarav' || ' Sharma' mean in MySQL with default settings?
In MySQL || is OR unless the PIPES_AS_CONCAT mode is on; use CONCAT().
3. Which is the SQL-standard way to create an auto-numbered column, used by PostgreSQL 10 and later?
GENERATED ... AS IDENTITY comes from the standard; SERIAL is PostgreSQL's older form.
4. What does 7 / 2 return in PostgreSQL, SQLite and SQL Server?
Dividing two integers gives an integer in those databases; MySQL returns 3.5000.
Learn SQL with a mentor
Free lessons get you started. Our PHP & MySQL Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
