SQL & Database — Tư tưởng & Nguyên lý
5/52
Bài 5 / 52~18 phútNền tảng & mô hình dữ liệuLộ trình · chặng 10/21Miễn phí lượt xem

SQL flavor map — 8 điểm khác biệt giữa các hệ quản trị

8 điểm khác biệt SQL giữa PG/MySQL/SQLite/MSSQL: auto-increment, boolean, UPSERT, trailing-space. 90% ANSI portable — 10% cần flag khi port code.

TL;DR: Mọi RDBMS (PostgreSQL, MySQL, SQLite, SQL Server) đều implement ANSI SQL làm nền — 90% query cơ bản (SELECT/WHERE/JOIN/GROUP BY/ORDER BY) portable cross-vendor. 10% còn lại là extension và quirk riêng từng engine. Bài này map 8 điểm khác biệt thực tế hay gặp: identifier quoting, string concat, auto-increment, pagination, boolean, identifier case, UPSERT, và string trailing-space compare. Ba điểm dễ gây silent bug nhất khi port code: identifier case (macOS OK, Linux fail), boolean semantics, và trailing-space mismatch. Khoá này dạy ANSI SQL; khi một feature thực sự khác giữa engine, bài học sẽ có callout ghi rõ.

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 PostgreSQL. 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 — đủ để các bài còn lại của khoá học chỉ cần callout ở đú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 DialectTiếng Anh tương đươngĐặc điểm
PostgreSQLBritish EnglishChuẩn nhất, tuân thủ ANSI, formal
MySQLAmerican EnglishPhổ biến nhất, một số quirk riêng
SQLiteSimplified EnglishGọn nhẹ, embedded, bỏ bớt tính năng nặng
MSSQLAustralian EnglishGần British nhưng có slang riêng ([bracket], + concat)
💡 Cách nhớ

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õ.

Điều đáng nhớ không phải danh sách extension của từng vendor — mà là cái giá phải trả khác nhau khi bạn bước ra khỏi lõi chung. Ba khác biệt dưới đây xếp theo mức ồn: hai cái đầu tự tố giác, cái thứ ba thì không:

Hộp lõi ANSI gồm SELECT WHERE JOIN GROUP BY ORDER BY toả xuống ba khác biệt khi chạm vùng riêng vendor: AUTO_INCREMENT và backtick quoting gây syntax error ngay, tên bảng Users so với users chạy tốt trên macOS nhưng vỡ khi deploy Linux, còn so sánh chuỗi có khoảng trắng cuối thì MySQL đệm nên khớp mà PostgreSQL không khớp và không có lỗi hay log nào

Tham khảo: Wikipedia — SQL Standardization history — overview lịch sử các phiên bản standard.

Thử ngẫmquery bạn viết tuần này dùng bao nhiêu phần trăm ANSI thuần so với extension riêng của engine? Đổi engine ngày mai, phần nào vỡ trước tiên?

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.

#CasePostgreSQLMySQLSQLiteMSSQL
1Identifier quoting"name" (double quote)`name` (backtick)"name" hoặc `name`[name]
2String concat'a' || 'b' (ANSI)CONCAT('a','b')'a' || 'b''a' + 'b'
3Auto-increment PKBIGSERIAL hoặc IDENTITYAUTO_INCREMENTINTEGER PRIMARY KEYIDENTITY(1,1)
4PaginationLIMIT n OFFSET mLIMIT m, n hoặc LIMIT n OFFSET mLIMIT n OFFSET mOFFSET m ROWS FETCH NEXT n ROWS ONLY
5Boolean typeNative BOOLEANTINYINT(1) (alias BOOL)INTEGER (0/1)BIT
6Identifier caseLowercase mặc định (quoted thì preserve)OS-dependentInsensitiveInsensitive
7UPSERTINSERT ... ON CONFLICT DO UPDATEINSERT ... ON DUPLICATE KEY UPDATEINSERT ... ON CONFLICT DO UPDATE (3.24+)MERGE
8String compare trailing space'a' = 'a ' → false'a' = 'a ' → truePhụ 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.

SQL
-- 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:

SQL
-- 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

SQL
-- 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.

SQL
-- 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.

SQL
-- 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.

SQL
-- 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.

Thử ngẫmdự án của bạn có test nào chạy đúng OS/engine production, hay chỉ test trên máy dev? lower_case_table_names khác nhau giữa hai môi trường đó có bị phát hiện không?

5. Extension quan trọng của từng engine

Ngoài 8 điểm khác biệt cốt lõi, mỗi RDBMS còn có extension riêng cho workload hiện đại. Đây là lý do chọn engine thường gắn với workload, không phải syntax:

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.

Một số extension đáng chú ý (engine-specific, sẽ nhắc khi liên quan trong khoá):

6. Cách khoá học này xử lý multi-engine

Khoá này dạy ANSI SQL làm nền — syntax và behavior của bài học áp dụng nguyên vẹn cho mọi RDBMS tuân thủ chuẩn. Ví dụ code trong bài dùng cú pháp chuẩn SQL khi có thể (ví dụ GENERATED ALWAYS AS IDENTITY thay vì BIGSERIAL, CURRENT_TIMESTAMP thay vì now()). Khi một feature thực sự khác giữa các engine, bài học sẽ có callout rõ:

MDX
<Callout type="info" title="🐘 Ghi chú dialect">
MySQL không hỗ trợ DISTINCT ON. Workaround: subquery với GROUP BY + JOIN.
PostgreSQL có ILIKE cho case-insensitive — portable hơn là dùng LOWER() LIKE.
</Callout>

Mọi bài còn lại dùng ANSI SQL 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

Pitfall — 3 trap phổ biến khi học SQL từ nhiều nguồn

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 Bool field 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

📚 Deep Dive — SQL standards & vendor differences

Tài liệu tham khảo:

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. Liên hệ các bài khác

11. 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
  • Engine extension (JSONB, vector search, RLS, FTS) là lý do chọn engine — đặc thù từng workload, không phải SQL syntax
  • 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 dạy ANSI SQL — callout dialect xuất hiện chỉ ở chỗ thực sự khác giữa engine

12. Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao MySQL chạy `SELECT * FROM Users` OK trên macOS dev nhưng fail trên Linux production?
  2. Q2
    Bạn port code từ MySQL sang Postgres. Ngoài syntax (AUTO_INCREMENT → BIGSERIAL), behavior nào dễ gây silent bug nhất?
  3. Q3
    ORM (Prisma/JPA) đã abstract syntax khác biệt vendor. Vì sao vẫn cần biết flavor differences?
  4. Q4
    Phâ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?
  5. Q5
    PG 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?

Bài tiếp theo: Module 2 — Truy vấn cơ bản — SELECT, projection & alias

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Các mô hình dữ liệu — relational, document, key-value, graph