Series này không dạy SQL theo kiểu chép cú pháp — mọi khái niệm đều đi kèm 1 engine SQLite thật (biên
dịch sang WebAssembly) chạy ngay trong trình duyệt của bạn, và 1 bộ dữ liệu kinh doanh xuyên suốt gọi
là TechMart (cửa hàng bán đồ điện tử/phụ kiện/gia dụng/văn phòng online) mà bạn sẽ
dùng lại ở toàn bộ 17 bài. Bài đầu tiên xây nền tảng bắt buộc: hiểu đúng dữ liệu quan hệ được tổ chức
thế nào, vì sao SQLite có hệ kiểu khác hẳn phần lớn database khác, và cách lọc/sắp xếp/phân trang dữ
liệu bằng SELECT.
1. Mô Hình Quan Hệ: Bảng, Hàng, Cột
Mô hình quan hệ (relational model) tổ chức dữ liệu thành các bảng (table) — mỗi bảng
có 1 tập cột (column) cố định, mỗi cột mang 1 ý nghĩa và kiểu dữ liệu riêng. Mỗi
hàng (row) trong bảng đại diện cho 1 thực thể cụ thể — 1 khách hàng, 1 sản phẩm, 1
đơn hàng. Khác với việc nhét dữ liệu tuỳ tiện vào 1 file text hay JSON lồng nhau không giới hạn, mô
hình quan hệ buộc dữ liệu phải có cấu trúc nhất quán: mọi hàng trong bảng
products đều có đúng các cột product_id, product_name,
category, unit_price, stock_quantity — không hàng nào thiếu
cột, không hàng nào có thêm cột lạ.
Bộ dữ liệu TechMart dùng xuyên suốt series gồm 4 bảng (2 bảng đầu dùng ngay ở bài này, 2 bảng sau dành
cho Bài 3 khi học JOIN):
| Bảng | Mô tả | Cột chính |
|---|---|---|
customers |
Khách hàng đã đăng ký tài khoản | customer_id, full_name, email, country, signup_date, is_active |
products |
Danh mục sản phẩm đang bán | product_id, product_name, category, unit_price, stock_quantity |
orders |
Đơn hàng (có thể không gắn khách hàng — đơn khách vãng lai) | order_id, customer_id, order_date, status, total_amount |
order_items |
Từng dòng sản phẩm cụ thể trong 1 đơn hàng | order_item_id, order_id, product_id, quantity, unit_price |
Câu lệnh cơ bản nhất để đọc dữ liệu là SELECT. Dấu * nghĩa là "lấy tất cả
các cột", theo đúng thứ tự đã khai báo khi tạo bảng:
SELECT * FROM products;
Thực tế hiếm khi dùng SELECT * trong code sản xuất (production) — liệt kê đúng cột cần
dùng giúp query rõ ràng hơn và tránh vỡ code khi bảng có thêm cột mới sau này:
SELECT product_name, unit_price FROM products;
2. Hệ Kiểu Động Của SQLite: Type Affinity
Đây là điểm khác biệt lớn nhất giữa SQLite và phần lớn database khác (PostgreSQL, MySQL) mà người mới
thường không biết: hầu hết database dùng kiểu tĩnh nghiêm ngặt — 1 cột khai báo
INTEGER chỉ được phép chứa số nguyên, chèn chuỗi vào sẽ báo lỗi ngay. SQLite hoạt động
hoàn toàn khác: nó dùng cơ chế gọi là type affinity (ái lực kiểu) — kiểu khai báo của
1 cột chỉ là "gợi ý ưu tiên", SQLite vẫn chấp nhận lưu hầu hết mọi kiểu dữ liệu vào bất kỳ cột nào.
Cụ thể, SQLite có đúng 5 storage class (kiểu lưu trữ thực tế của 1 giá trị):
NULL, INTEGER, REAL, TEXT, BLOB. Và
có 5 type affinity (ái lực kiểu của 1 cột, suy ra từ chuỗi kiểu khai báo lúc
CREATE TABLE):
| Type Affinity | Suy ra khi chuỗi kiểu khai báo chứa | Ví dụ kiểu khai báo |
|---|---|---|
INTEGER |
chứa "INT" |
INTEGER, INT, BIGINT |
TEXT |
chứa "CHAR", "CLOB", hoặc "TEXT" |
TEXT, VARCHAR(255), NCHAR |
REAL |
chứa "REAL", "FLOA", hoặc "DOUB" |
REAL, FLOAT, DOUBLE PRECISION |
BLOB |
không khai báo kiểu, hoặc chứa "BLOB" |
BLOB, để trống |
NUMERIC |
mọi trường hợp còn lại | NUMERIC, DECIMAL(10,2), BOOLEAN, DATE |
typeof() để nhìn thấy storage class thật
typeof(x) trả về đúng storage class SQLite thực sự dùng để lưu giá trị
x — không phải kiểu bạn khai báo lúc tạo bảng. Chạy
SELECT product_id, typeof(product_id) FROM products LIMIT 1; trong sân chơi bên dưới,
bạn sẽ thấy INTEGER — đúng như kỳ vọng vì cột này chỉ chứa số nguyên. Nhưng vì
total_amount trong bảng orders khai báo REAL, SQLite vẫn hoàn
toàn cho phép bạn INSERT 1 chuỗi văn bản vào đó — typeof() lúc này sẽ trả
về TEXT thay vì REAL, và mọi phép toán số học trên giá trị đó có thể cho
kết quả bất ngờ.
signup_date và order_date khai báo TEXT?
'YYYY-MM-DD' với affinity
TEXT — vì chuỗi ISO-8601 vừa đọc được bằng mắt thường, vừa so sánh/sắp xếp đúng thứ tự
thời gian bằng phép so sánh chuỗi thông thường ('2026-03-01' > '2026-01-01' đúng như
so sánh ngày thật). PostgreSQL, ngược lại, có kiểu DATE/TIMESTAMP
thật — đây là 1 khác biệt dialect quan trọng bạn sẽ gặp lại khi thực hành Docker ở
Bài 2.
INTEGER (nghĩ rằng số điện thoại "là số"), giá trị
0912345678 sẽ mất số 0 ở đầu — vì INTEGER affinity buộc SQLite chuyển
chuỗi số hợp lệ thành số nguyên thật trước khi lưu, và số 0 đầu không có ý nghĩa toán học. Số điện
thoại, mã bưu điện, hay mọi "chuỗi trông giống số" nhưng không dùng để tính toán nên luôn khai báo
TEXT.
3. WHERE: Lọc Dữ Liệu & Logic 3 Trị
WHERE giữ lại đúng những hàng thoả điều kiện. SQLite hỗ trợ đầy đủ toán tử so sánh
(=, !=/<>, <, >,
<=, >=) và toán tử logic (AND, OR,
NOT). Dưới đây là 4 tình huống thực tế minh hoạ các toán tử khác nhau trên cùng bộ dữ
liệu TechMart:
Tình huống 1 — Báo cáo doanh thu: tìm đơn hàng đã giao thành công với giá trị lớn.
SELECT order_id, order_date, total_amount
FROM orders
WHERE status = 'delivered' AND total_amount > 200;
Tình huống 2 — Quản lý tồn kho: lọc theo nhiều danh mục cùng lúc bằng
IN.
SELECT product_name, category, stock_quantity
FROM products
WHERE category IN ('Electronics', 'Office');
Tình huống 3 — Marketing/phân khúc khách hàng: tìm khách dùng Gmail bằng
LIKE.
SELECT full_name, email
FROM customers
WHERE email LIKE '%@gmail.com';
% khớp bất kỳ chuỗi con nào (kể cả rỗng), _ khớp đúng 1 ký tự. Muốn tìm đúng
ký tự % hoặc _ theo nghĩa đen, dùng mệnh đề ESCAPE, ví dụ
LIKE '50\%%' ESCAPE '\' tìm chuỗi bắt đầu bằng "50%".
Tình huống 4 — Phát hiện dữ liệu bất thường: tìm đơn hàng của khách vãng lai (không gắn tài khoản).
SELECT order_id, order_date, total_amount
FROM orders
WHERE customer_id IS NULL;
IS NULL, không viết = NULL?
TRUE, FALSE, hoặc UNKNOWN — thay vì chỉ đúng/sai như logic
thông thường. NULL đại diện cho "giá trị không xác định", nên
so sánh bất kỳ thứ gì với NULL — kể cả NULL với chính nó — đều cho kết quả UNKNOWN,
không phải TRUE. Viết WHERE customer_id = NULL sẽ luôn cho ra 0 hàng, vì
mọi so sánh đều là UNKNOWN (và WHERE chỉ giữ lại hàng có kết quả đúng
TRUE, loại cả FALSE lẫn UNKNOWN). Đây là lý do SQL có hẳn 2
toán tử riêng IS NULL/IS NOT NULL để kiểm tra NULL đúng cách — không dùng
được toán tử so sánh thông thường.
Tình huống 5 — Báo cáo theo kỳ: lọc đơn hàng trong 1 khoảng thời gian bằng
BETWEEN.
SELECT order_id, order_date, total_amount
FROM orders
WHERE order_date BETWEEN '2026-01-01' AND '2026-03-31'
ORDER BY order_date;
BETWEEN a AND b tương đương >= a AND <= b — bao gồm cả 2 đầu mút. Vì
order_date lưu dạng chuỗi ISO-8601, so sánh chuỗi ở đây cho đúng kết quả so sánh ngày
tháng thật (xem lại callout mục 2).
4. ORDER BY, LIMIT, OFFSET: Sắp Xếp & Phân Trang
ORDER BY sắp xếp kết quả theo 1 hoặc nhiều cột, ASC (tăng dần, mặc định)
hoặc DESC (giảm dần). LIMIT n chỉ lấy n hàng đầu tiên,
OFFSET m bỏ qua m hàng đầu — kết hợp cả 2 tạo ra cơ chế phân trang kinh
điển: trang N (kích thước p) → LIMIT p OFFSET (N-1)*p.
Tình huống 6 — Bảng xếp hạng: top 5 đơn hàng giá trị cao nhất.
SELECT order_id, total_amount
FROM orders
ORDER BY total_amount DESC
LIMIT 5;
Tình huống 7 — Phân trang danh sách sản phẩm: trang 2, 5 sản phẩm/trang.
SELECT product_id, product_name
FROM products
ORDER BY product_id
LIMIT 5 OFFSET 5;
ORDER BY status, total_amount DESC sắp xếp trước theo status (alphabet),
trong mỗi nhóm status giống nhau mới sắp tiếp theo total_amount giảm dần. Luôn thêm 1
cột "duy nhất" (như khoá chính) làm tiêu chí cuối cùng nếu cần thứ tự
ổn định, lặp lại giống hệt mỗi lần chạy — nếu không, khi nhiều hàng có cùng giá trị
sắp xếp, SQLite không đảm bảo thứ tự giữa chúng sẽ giống nhau qua các lần chạy khác nhau.
Về vị trí NULL khi sắp xếp: SQLite coi NULL là giá trị nhỏ nhất — mặc
định ORDER BY col ASC đưa hàng có NULL lên đầu, DESC đưa xuống
cuối. Muốn kiểm soát rõ ràng, dùng thêm NULLS FIRST/NULLS LAST (hỗ trợ từ
SQLite 3.30+).
Sân chơi tương tác: SQL Workbench Trên Dataset TechMart
Engine bên dưới là SQLite thật (biên dịch WebAssembly qua sql.js), không phải mô phỏng —
mọi câu query chạy đúng như trên máy bạn cài SQLite thật. Bấm 1 nút tình huống để nạp sẵn query vào ô
soạn thảo, chỉnh sửa tuỳ ý rồi bấm "Chạy" (hoặc Ctrl+Enter).
customers
| Cột | Kiểu |
|---|---|
| customer_id | INTEGER PRIMARY KEY |
| full_name | TEXT NOT NULL |
| TEXT | |
| country | TEXT |
| signup_date | TEXT |
| is_active | INTEGER |
products
| Cột | Kiểu |
|---|---|
| product_id | INTEGER PRIMARY KEY |
| product_name | TEXT NOT NULL |
| category | TEXT |
| unit_price | REAL |
| stock_quantity | INTEGER |
orders
| Cột | Kiểu |
|---|---|
| order_id | INTEGER PRIMARY KEY |
| customer_id | INTEGER (NULL = khách vãng lai) |
| order_date | TEXT |
| status | TEXT |
| total_amount | REAL |
order_items (dùng từ Bài 3)
| Cột | Kiểu |
|---|---|
| order_item_id | INTEGER PRIMARY KEY |
| order_id | INTEGER |
| product_id | INTEGER |
| quantity | INTEGER |
| unit_price | REAL |
Đang tải...
⬇ Tải file schema + dữ liệu mẫu TechMart (.sql) — chạy
được trực tiếp bằng sqlite3 CLI hoặc DB Browser for SQLite trên máy bạn.
Trắc nghiệm ôn tập
Câu 1: Khác biệt cốt lõi giữa "type affinity" của SQLite và kiểu tĩnh nghiêm ngặt của PostgreSQL là gì?
Trắc nghiệm ôn tập
Câu 2: Vì sao WHERE customer_id = NULL luôn trả về 0 hàng, kể cả khi có hàng
customer_id thực sự là NULL?
Trắc nghiệm ôn tập
Câu 3: Muốn lấy đúng 10 sản phẩm cho trang 3 (mỗi trang 10 sản phẩm), câu lệnh nào đúng?
Trắc nghiệm ôn tập
Câu 4: Vì sao TechMart lưu order_date dạng chuỗi 'YYYY-MM-DD' (ISO-8601)
thay vì định dạng khác như 'DD/MM/YYYY'?
Trắc nghiệm ôn tập
Câu 5: Tại sao nên tránh lưu số điện thoại như 0912345678 vào 1 cột có type affinity
INTEGER?