SQL flavor map — Postgres khác MySQL ở 8 chỗ phải nhớ
Identifier quoting, string concat, auto-increment, LIMIT, boolean, case sensitivity, UPSERT, string compare. Bảng so sánh PG/MySQL/SQLite/MSSQL — đủ để khoá học chỉ flag chỗ khác.
Bạn copy một query từ Stack Overflow — câu trả lời có 500 upvote, tag MySQL — và chạy thử trên Postgres. Kết quả trả về ngay: ERROR: syntax error at or near "AUTO_INCREMENT". Cùng là SQL, cùng là SELECT/INSERT, nhưng dialect khác nhau khiến syntax hợp lệ ở vendor này thành lỗi compile ở vendor kia.
Bài này map 8 chỗ chính các vendor khác biệt — đủ để 80 bài còn lại của khoá học bạn chỉ cần đọc callout "MySQL khác:" ở đúng chỗ thực sự khác. Phần lớn syntax PG/MySQL/SQLite portable khoảng 90% — 10% còn lại mới cần flag.
1. Analogy — 4 dialect tiếng Anh
Hãy nghĩ SQL như tiếng Anh: cùng một ngôn ngữ, nhưng British English, American English, Australian English và Singapore English có những từ và cách nói riêng. Người nói dialect nào cũng giao tiếp được với nhau — nhưng một số cụm dùng ở Anh sẽ khiến người Mỹ nhìn chằm chằm.
| SQL Dialect | Tiếng Anh tương đương | Đặc điểm |
|---|---|---|
| PostgreSQL | British English | Chuẩn nhất, tuân thủ ANSI, formal |
| MySQL | American English | Phổ biến nhất, một số quirk riêng |
| SQLite | Simplified English | Gọn nhẹ, embedded, bỏ bớt tính năng nặng |
| MSSQL | Australian English | Gần British nhưng có slang riêng ([bracket], + concat) |
PG = British English: chuẩn, formal, đúng spec. MySQL = American: phổ biến, một số quirk. SQLite = đơn giản hoá cho embedded. MSSQL = gần PG nhưng có syntax riêng. Cùng hiểu nhau 90% — 10% còn lại chính là 8 điểm bài này map.
2. ANSI SQL standard — 90% giống nhau
SQL có lịch sử chuẩn hoá dài: ANSI SQL 1986 là phiên bản đầu tiên, theo sau là SQL-92 (bổ sung lớn — JOIN, subquery), SQL:1999 (recursive CTE, trigger), SQL:2003 (window function, sequence), SQL:2008, SQL:2011 (temporal), SQL:2016 (JSON path), SQL:2023 (property graph, JSON). Mọi vendor implement một subset của standard này, cộng thêm extension riêng.
Hệ quả thực tế: query cơ bản — SELECT/WHERE/JOIN/GROUP BY/ORDER BY/LIMIT — gần như portable giữa PG, MySQL, và SQLite. Sự khác biệt nằm ở extension vendor-specific và một số quyết định thiết kế sớm trước khi standard được định nghĩa rõ.
Tham khảo: Wikipedia — SQL Standardization history — overview lịch sử các phiên bản standard.
3. 8 chỗ khác biệt — bảng tổng hợp
Bảng dưới map 8 case thực tế hay gặp. Đọc một lần để có mental map — không cần học thuộc, bài sau sẽ flag lại ở đúng chỗ cần.
| # | Case | PostgreSQL | MySQL | SQLite | MSSQL |
|---|---|---|---|---|---|
| 1 | Identifier quoting | "name" (double quote) | `name` (backtick) | "name" hoặc `name` | [name] |
| 2 | String concat | 'a' || 'b' (ANSI) | CONCAT('a','b') | 'a' || 'b' | 'a' + 'b' |
| 3 | Auto-increment PK | BIGSERIAL hoặc IDENTITY | AUTO_INCREMENT | INTEGER PRIMARY KEY | IDENTITY(1,1) |
| 4 | Pagination | LIMIT n OFFSET m | LIMIT m, n hoặc LIMIT n OFFSET m | LIMIT n OFFSET m | OFFSET m ROWS FETCH NEXT n ROWS ONLY |
| 5 | Boolean type | Native BOOLEAN | TINYINT(1) (alias BOOL) | INTEGER (0/1) | BIT |
| 6 | Identifier case | Lowercase mặc định (quoted thì preserve) | OS-dependent | Insensitive | Insensitive |
| 7 | UPSERT | INSERT ... ON CONFLICT DO UPDATE | INSERT ... ON DUPLICATE KEY UPDATE | INSERT ... ON CONFLICT DO UPDATE (3.24+) | MERGE |
| 8 | String compare trailing space | 'a' = 'a ' → false | 'a' = 'a ' → true | Phụ thuộc COLLATE | 'a' = 'a ' → true |
3.1 Chi tiết — Identifier quoting
PG tuân thủ ANSI SQL: identifier không phải keyword thì không cần quote; nếu muốn preserve case hoặc dùng reserved word làm tên, dùng double quote. MySQL chọn backtick — style này không ANSI, nhưng tránh conflict với string literal.
-- PostgreSQL: double quote de preserve case
SELECT "firstName", "lastName" FROM users;
-- MySQL: backtick
SELECT `firstName`, `lastName` FROM users;
-- Khong quote (an toan nhat, lowercase identifier): portable giua PG va MySQL
SELECT first_name, last_name FROM users;
3.2 Chi tiết — Auto-increment PK
Đây là case hay gặp nhất khi copy Stack Overflow:
-- MySQL syntax -- KHONG chay tren PG
CREATE TABLE products (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255)
);
-- PostgreSQL equivalent -- BIGSERIAL la shorthand cho sequence
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name TEXT
);
-- PG 10+: IDENTITY (ANSI SQL:2003 compliant, prefer over SERIAL)
CREATE TABLE products (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT
);
-- SQLite: INTEGER PRIMARY KEY tu dong la alias cua rowid
CREATE TABLE products (
id INTEGER PRIMARY KEY,
name TEXT
);
3.3 Chi tiết — Pagination
-- PostgreSQL va SQLite: LIMIT n OFFSET m (ANSI-ish)
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20;
-- MySQL: ho tro ca hai, nhung co syntax rieng (m, n thay vi n OFFSET m)
SELECT * FROM products ORDER BY id LIMIT 20, 10; -- MySQL: offset truoc, limit sau
SELECT * FROM products ORDER BY id LIMIT 10 OFFSET 20; -- cung chay tren MySQL
-- MSSQL: ANSI SQL:2008 syntax (verbose hon)
SELECT * FROM products ORDER BY id
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;
4. Top 3 khác biệt thực sự gây bug khi port code
Trong 8 case trên, 3 case dễ gây bug im lặng (silent bug) nhất khi chuyển code từ vendor này sang vendor kia:
4.1 Identifier case (case 6) — deploy lên Linux thì vỡ
MySQL trên macOS và Windows so sánh tên bảng/tên cột không phân biệt hoa thường (case-insensitive). Trên Linux, MySQL thường mặc định case-sensitive (lower_case_table_names=0) — nhưng một số bản cài qua package manager (Ubuntu apt, một số image MySQL 8) có thể set khác, nên kiểm tra SHOW VARIABLES LIKE 'lower_case_table_names' trên server thực tế. PostgreSQL luôn lowercase unquoted identifier — users, Users, USERS đều thành users — nhưng nếu table tạo với quoted identifier "Users", thì chỉ "Users" match.
-- MySQL macOS: ca hai chay OK
SELECT * FROM Users;
SELECT * FROM users;
-- MySQL Linux: phu thuoc bien he thong lower_case_table_names
-- Neu lower_case_table_names=0 (default Linux): "Users" != "users" -> loi
-- PostgreSQL: unquoted identifier luon lowercase
-- Table tao bang: CREATE TABLE users (...)
-- SELECT * FROM Users; -> PostgreSQL convert "Users" thanh "users" -> OK
-- SELECT * FROM "Users"; -> tim "Users" (preserve case) -> relation does not exist
Hệ quả: code chạy OK trên macOS dev (MySQL), deploy Linux production → Table 'mydb.Users' doesn't exist. Fix: convention luôn dùng snake_case lowercase, không quote identifier.
4.2 Boolean (case 5) — port sang PG cần migrate column
MySQL không có native boolean — BOOL là alias của TINYINT(1), lưu giá trị 0 hoặc 1. Khi port sang PG, cần migrate column type và cả application code nếu code đang so sánh = 1 thay vì = true.
-- MySQL: BOOL la TINYINT(1)
CREATE TABLE tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
is_done BOOL -- luu 0 hoac 1
);
-- Query MySQL app code co the dung:
SELECT * FROM tasks WHERE is_done = 1;
-- PostgreSQL: BOOLEAN native
CREATE TABLE tasks (
id BIGSERIAL PRIMARY KEY,
is_done BOOLEAN -- true hoac false
);
-- PG khong chap nhan WHERE is_done = 1 (type mismatch)
-- Phai viet:
SELECT * FROM tasks WHERE is_done = true;
SELECT * FROM tasks WHERE is_done; -- shorthand
4.3 String compare với trailing space (case 8) — silent data mismatch
Đây là case nguy hiểm nhất vì không gây lỗi — chỉ trả về kết quả sai.
-- MySQL: padding compare, 'AB' = 'AB ' tra ve true
SELECT 'AB' = 'AB '; -- MySQL: 1 (true)
-- PostgreSQL: no padding, 'AB' = 'AB ' tra ve false
SELECT 'AB' = 'AB '; -- PG: false
-- Scenario: data trong MySQL co the chua row voi code = 'AB '
-- Port sang PG, query WHERE code = 'AB' dot nhien khong match row do
-- Silent bug: khong bao gio biet data o do, chi thay count giam
Fix: luôn TRIM() data trước khi lưu, hoặc dùng constraint để chuẩn hoá dữ liệu.
5. PG-specific extension — forward links các Module sau
PG không chỉ implement ANSI SQL tốt hơn — nó còn có nhiều extension mà MySQL không hỗ trợ native. Đây là lý do PG đang chiếm thị phần trong dev community.
Theo Stack Overflow Developer Survey 2025, PostgreSQL đạt 55.6% — lần đầu vượt MySQL (39.6%) trong database phổ biến nhất với developer. Xu hướng này bắt đầu từ khoảng 2020 khi ecosystem extension của PG trở nên đặc biệt mạnh cho workload hiện đại.
5 extension đáng chú ý sẽ học trong khoá này:
JSONB(Module 9 của khoá này) — JSON binary indexable, query JSON như relational datapgvector(Module 9 của khoá này) — vector similarity search cho AI/ML workloadpg_trgm(Module 9 của khoá này) — trigram similarity cho fuzzy text search- Window functions nâng cao (Module 8 của khoá này) —
DISTINCT ON,NTILE, frame specification chi tiết - Row-Level Security (Module 10 của khoá này) — policy-based access control tại mức database row
6. Cách khoá học này xử lý multi-engine
Khoá này baseline PostgreSQL. 90% syntax và behavior của bài học áp dụng nguyên vẹn cho MySQL và SQLite. Khi syntax hoặc behavior thực sự khác giữa PG và MySQL, sẽ có callout:
<Callout type="info" title="MySQL khác:">
MySQL không hỗ trợ DISTINCT ON. Workaround: subquery với GROUP BY + JOIN.
</Callout>
Mọi bài còn lại assume PG syntax trừ khi có callout rõ ràng. SQLite và MSSQL chỉ flag khi behavior cực kỳ khác và quan trọng với người học.
7. Pitfall — 3 trap khi học SQL multi-vendor
Trap 1 — Học thuộc syntax 1 vendor rồi nghĩ đó là "SQL chuẩn".
MySQL AUTO_INCREMENT, backtick identifier, BOOL là MySQL-specific. PG BIGSERIAL, JSONB, ON CONFLICT là PG-specific. Không có vendor nào implement 100% ANSI SQL — và không vendor nào chỉ implement ANSI SQL. Khi học từ một nguồn, luôn kiểm tra tag vendor của nguồn đó.
Trap 2 — Copy-paste Stack Overflow không check vendor tag.
Stack Overflow câu hỏi thường tag mysql hoặc postgresql — nhưng người dùng hay bỏ qua. Query AUTO_INCREMENT có thể có 800 upvote — vẫn không chạy được trên PG. Habit: kiểm tra tag vendor trước khi copy.
Trap 3 — Test local macOS MySQL, deploy Linux production.
MySQL identifier case-insensitive trên macOS, case-sensitive trên Linux. Code SELECT * FROM Users chạy OK local, deploy Linux → Table doesn't exist. Fix dứt điểm: convention snake_case lowercase toàn bộ, không dùng quoted identifier với mixed case.
8. Applied — đọc docs vendor mình đang dùng
Mỗi vendor có official documentation riêng, cập nhật theo phiên bản. Nếu code production trên PG, bookmark postgresql.org/docs/current — đây là source of truth cho mọi tính năng PG.
Nguyên tắc: 5 phút đọc docs chính thức tiết kiệm hơn 30 phút debug syntax error. Khi không chắc một tính năng có portable không, kiểm tra docs của vendor bạn đang dùng trước khi thử.
ORM và behavior khác biệt:
ORM (Prisma, JPA, SQLAlchemy, ActiveRecord) abstract được phần lớn syntax khác biệt — bạn viết code ORM, ORM generate SQL phù hợp từng vendor. Nhưng ORM không abstract được behavior khác biệt:
- Case sensitivity của identifier: ORM generate tên bảng đúng, nhưng nếu database và OS config khác nhau, vẫn có thể fail
- Boolean semantics: ORM map
Boolfield sang đúng column type, nhưng raw query bypass ORM vẫn cần cẩn thận - String padding compare: ORM không tự
TRIM()— data đã có trailing space vẫn gây mismatch
Khi đổi vendor production (dù có ORM), vẫn cần kiểm tra 8 điểm trên bằng integration test thực sự.
9. Deep Dive — SQL standards và vendor differences
Tài liệu tham khảo:
- PostgreSQL Documentation — Appendix D "SQL Conformance" — list chính thức những phần ANSI SQL PG hỗ trợ và không hỗ trợ. Dùng khi cần biết chính xác một feature có portable không: tìm feature trong Appendix D, xem status "supported"/"not supported"/"partially supported".
- Modern SQL by Markus Winand — track compliance từng vendor cho từng feature SQL mới (window function, MERGE, JSON path, temporal, property graph). Free web. Dùng khi muốn xem ai support feature mới nhất của SQL standard.
- Database System Concepts — Silberschatz, Korth, Sudarshan (7th ed) — Ch.3-4 "Introduction to SQL / Intermediate SQL" — ANSI SQL không bias vendor. Dùng khi muốn học SQL theo standard, không bị ảnh hưởng bởi quirk của vendor cụ thể.
Ghi chú: PG Appendix D khi cần check portability của một feature cụ thể. Modern SQL khi muốn xem vendor nào support feature mới của standard. DSC Ch.3-4 nếu muốn nền tảng SQL lý thuyết không vendor-specific.
10. Tóm tắt
- ANSI SQL standard (1986 → 2023) là chuẩn — mọi vendor implement subset + extension riêng; 90% query cơ bản portable
- 8 chỗ khác biệt chính: identifier quoting, string concat, auto-increment, LIMIT pagination, boolean type, identifier case, UPSERT syntax, string padding compare
- 3 trap thực sự gây bug khi port code: identifier case (macOS OK, Linux fail), boolean type mismatch, string trailing-space compare silent mismatch
- PG ecosystem extension là lý do dominance từ 2020: JSONB, pgvector, pg_trgm, window functions nâng cao, Row-Level Security
- ORM abstract syntax khác biệt nhưng không abstract behavior khác biệt — vẫn cần biết flavor differences khi đổi vendor
- Khoá học flag bằng
<Callout title="MySQL khác:">chỉ ở chỗ thực sự khác — phần còn lại assume PG - Forward links: Module 9 (JSONB/pgvector/pg_trgm) và Module 10 (Row-Level Security) của khoá này
11. Tự kiểm tra
Q1Vì sao MySQL chạy `SELECT * FROM Users` OK trên macOS dev nhưng fail trên Linux production?▸
MySQL trên macOS và Windows mặc định so sánh tên bảng case-insensitive — Users, users, USERS đều match cùng một bảng. Trên Linux, MySQL mặc định dùng biến hệ thống lower_case_table_names=0 — case-sensitive. Bảng tạo tên users không match query FROM Users.
Fix dứt điểm: convention toàn bộ identifier snake_case lowercase — không dùng CamelCase hay PascalCase cho tên bảng/cột. Khi mọi thứ đều lowercase, case sensitivity không còn là vấn đề dù chạy trên OS nào.
Q2Bạn port code từ MySQL sang Postgres. Ngoài syntax (AUTO_INCREMENT → BIGSERIAL), behavior nào dễ gây silent bug nhất?▸
Hai behavior hay gây silent bug nhất:
- String trailing-space compare: MySQL so sánh
'AB' = 'AB 'trả về true (padding). PG trả về false. Nếu database MySQL có row vớicode = 'AB '(trailing space), sau khi port sang PG, queryWHERE code = 'AB'đột nhiên không match row đó. Không có lỗi — chỉ count giảm không giải thích được. - Boolean semantics: Code MySQL so sánh
is_done = 1không chạy trên PG vìBOOLEANkhông nhận integer. Đây là type error — ít nhất PG báo lỗi rõ. Nguy hiểm hơn là ORM tự convert, dẫn đến behavior khác nhau giữa hai vendor mà không ai để ý.
Q3ORM (Prisma/JPA) đã abstract syntax khác biệt vendor. Vì sao vẫn cần biết flavor differences?▸
ORM abstract syntax: bạn viết model.findMany({ where: { isDone: true } }, ORM generate SQL phù hợp vendor. Nhưng ORM không abstract behavior:
- Raw query: khi ORM không đủ expressiveness, bạn drop xuống raw SQL — lúc đó phải biết vendor syntax.
- Data đã có từ trước: trailing space trong data cũ, boolean lưu dưới dạng 0/1 — ORM không tự clean data khi migrate.
- Isolation level default: PG default
READ COMMITTED, MySQL InnoDB cũng vậy — nhưng một số edge case behavior khác nhau. ORM không expose điều này. - Debug: khi query trả sai kết quả, phải hiểu SQL thực sự được generate và behavior của vendor để diagnose.
Q4Phân biệt khi nào LIMIT n OFFSET m đủ và khi nào cần keyset pagination. Flavor nào của SQL hỗ trợ cả hai?▸
LIMIT/OFFSET đủ khi: dataset nhỏ (dưới vài chục nghìn row), pagination không cần real-time (data không thay đổi giữa các page), hoặc admin UI không cần nhảy page nhanh. Cú pháp portable giữa PG, MySQL, SQLite.
Keyset pagination cần thiết khi: dataset lớn (hàng triệu row), OFFSET lớn khiến database phải scan và bỏ qua hàng trăm nghìn row trước khi trả kết quả — chậm và không scale. Keyset dùng điều kiện WHERE id > last_seen_id ORDER BY id LIMIT n — luôn O(log n) bất kể page nào.
PG, MySQL, và SQLite đều hỗ trợ cả hai approach (keyset chỉ là WHERE + ORDER BY, không cần syntax đặc biệt). MSSQL cũng hỗ trợ. Bài M02.3 của khoá này sẽ đi sâu vào tradeoff và implementation.
Q5PG có JSONB, pgvector, RLS mà MySQL không hỗ trợ native. Có nên migrate từ MySQL sang PG chỉ vì 3 tính năng này không?▸
Không nên migrate chỉ vì tính năng — migrate vì workload yêu cầu. Câu hỏi đúng là: ứng dụng của bạn có thực sự cần 3 tính năng đó không?
- JSONB: Nếu app đang dùng MySQL + lưu JSON trong TEXT column và cần query bên trong JSON — JSONB có giá trị. Nếu JSON chỉ đọc/ghi nguyên vẹn không query, TEXT hoặc MySQL JSON type đủ.
- pgvector: Chỉ cần khi app có embedding/similarity search. MySQL không hỗ trợ native — phải dùng external vector DB (Pinecone, Qdrant). Nếu đang dùng external vector DB, migrate sang PG + pgvector có thể đơn giản hoá stack.
- RLS: Cần khi multi-tenant app muốn enforce data isolation tại database layer thay vì application layer. Nếu isolation đang handle tốt ở application, RLS là nice-to-have.
Migration database production có rủi ro và chi phí cao (downtime, data migration, test regression). Chỉ migrate khi benefit rõ ràng vượt cost.
Bài tiếp theo: Module 2 — Truy vấn cơ bản — SELECT, WHERE, NULL three-valued logic
Bài này có giúp bạn hiểu bản chất không?