1. Vì sao có Database?
Hãy tưởng tượng bạn xây 1 app to-do list cho riêng mình. Bạn lưu mọi task vào file tasks.json:
[
{ "id": 1, "title": "Học SQL", "done": false },
{ "id": 2, "title": "Đi siêu thị", "done": true }
]
App chạy ngon. Một mình bạn dùng. Nhưng giờ tưởng tượng:
- Có 1000 user — file phải lưu task của ai? Tách 1000 file? Hay 1 file khổng lồ?
- Hai tab cùng mở — A đánh dấu task #5 done, B sửa title task #5. Ai ghi sau, ai thắng?
- Bạn muốn báo cáo: "User nào hoàn thành nhiều task nhất tháng này?" — bạn phải load toàn bộ file vào RAM rồi loop.
- Server crash giữa lúc đang ghi — file bị cắt nửa, JSON parse lỗi, mất sạch dữ liệu.
- Khi file lên 10GB, mở mất 30 giây.
Mỗi vấn đề trên là một tính năng cốt lõi mà DBMS đã giải quyết:
| Vấn đề | Tính năng DBMS |
|---|---|
| Truy vấn nhanh trên dữ liệu lớn | Index (B-tree, Hash...) |
| Nhiều client cùng ghi | Transactions, Locking, MVCC |
| Crash an toàn | Write-Ahead Log (WAL), Atomic commit |
| Truy vấn linh hoạt | SQL declarative + Query Optimizer |
| Đảm bảo ràng buộc dữ liệu | Constraints (FK, UNIQUE, CHECK) |
| Khôi phục dữ liệu cũ | Backup + Point-in-Time Recovery |
2. Lịch sử các mô hình dữ liệu
Hiểu vì sao mô hình quan hệ thắng, ta cần biết các mô hình trước nó:
2.1. File systems (1950s–60s)
Mỗi app tự quản lý file phẳng. Không có cấu trúc chung. Hai app cùng đọc → conflict. Khó query.
2.2. Hierarchical model (IBM IMS, 1966)
Dữ liệu hình cây cha-con. Tốt cho dữ liệu phân cấp tự nhiên (org chart, file system) nhưng cứng nhắc — đổi quan hệ là phải refactor cấu trúc.
2.3. Network model (CODASYL, 1971)
Cải tiến: cho phép nhiều quan hệ (n-n). Mỗi record kết nối qua "set" pointer. Mạnh hơn nhưng navigation phức tạp — phải code đường đi cụ thể từng truy vấn.
2.4. Relational model (Codd, 1970)
Edgar F. Codd, nhà toán học làm tại IBM, đề xuất mô hình quan hệ trong bài báo "A Relational Model of Data for Large Shared Data Banks". Ý tưởng cách mạng: tất cả dữ liệu đều biểu diễn được bằng các bảng, và truy vấn là phép toán đại số trên bảng (đại số quan hệ).
3 lợi thế then chốt:
- Đơn giản — bảng có dòng và cột. Ai cũng hiểu.
- Declarative — bạn nói cần gì, không cần nói cách nào. DB tự tối ưu.
- Linh hoạt — JOIN cho phép tổ hợp bảng theo ý muốn mà không cần biết trước trong thiết kế.
2.5. Object-Oriented (1990s)
Cố nhét OOP vào DB: Object = row. Phổ biến chỉ trong giới Java/C++ một thời. Hiện tại dùng ORM (Object-Relational Mapping) là cách phổ biến hơn.
2.6. NoSQL (2009+)
Khi web scale (Google, Amazon) cần nhiều TB và millions QPS, mô hình quan hệ + JOIN bị bottleneck. NoSQL hy sinh JOIN/ACID đổi lấy scale ngang. Có 4 họ: Key-Value, Document, Wide-column, Graph (sẽ học ở Ch9).
2.7. NewSQL (2012+)
"Có cách nào của cả hai thế giới không?" — Spanner, CockroachDB, YugabyteDB: scale ngang như NoSQL, đảm bảo ACID + SQL như Postgres. Phức tạp hơn, đắt hơn, dùng khi thực sự cần.
3. Mô hình quan hệ — đại số trên bảng
Trong lý thuyết, một relation là một tập các tuple tuân theo cùng một schema. Trong thực hành, đó là một bảng:
users:
┌────┬───────────────────┬────────────┐
│ id │ email │ created_at │
├────┼───────────────────┼────────────┤
│ 1 │ alice@example.com │ 2026-01-15 │
│ 2 │ bob@example.com │ 2026-02-03 │
│ 3 │ carol@example.com │ 2026-02-20 │
└────┴───────────────────┴────────────┘
Vậy relation có 6 phép toán cơ bản (đại số quan hệ):
| Phép | Ký hiệu | Mô tả | SQL tương ứng |
|---|---|---|---|
| Selection | σ | Lọc dòng theo điều kiện | WHERE |
| Projection | π | Chọn các cột | SELECT col1, col2 |
| Union | ∪ | Hợp 2 bảng (cùng schema) | UNION |
| Difference | − | Hiệu 2 bảng | EXCEPT |
| Cartesian Product | × | Tích Đề-các 2 bảng | CROSS JOIN |
| Rename | ρ | Đổi tên bảng/cột | AS |
Các phép phụ (derived) — kết hợp từ 6 phép trên:
- Intersection (∩) — giao 2 bảng →
INTERSECT - Join (⋈) — Cartesian + Selection →
JOIN ... ON - Division (÷) — "tất cả X có quan hệ với mọi Y" — phức tạp hiếm gặp
Ý nghĩa thực tế: mọi câu SQL phức tạp đều có thể chuyển về một biểu thức đại số quan hệ. Đó là cách query optimizer biến đổi và tối ưu query mà vẫn giữ kết quả đúng.
3.1. Codd's 12 rules (1985)
Codd liệt kê 12 quy tắc để gọi 1 DB là "thực sự quan hệ". Tóm gọn 5 ý quan trọng:
- Information rule — mọi dữ liệu (bao gồm metadata) lưu dưới dạng giá trị trong bảng.
- Guaranteed access — mọi giá trị truy cập được qua (table, primary key, column).
- Systematic NULL — NULL là giá trị riêng biệt với "0" hay "rỗng".
- Catalog — metadata cũng nằm trong bảng (thư mục hệ thống).
- Integrity independence — constraint ràng buộc lưu trong DB, không phụ thuộc app.
Lưu ý: không DBMS thương mại nào tuân thủ 100% 12 rules này. Nhưng Postgres rất gần.
4. Thuật ngữ chính
| Thuật ngữ học thuật | Tên thực hành | Ví dụ |
|---|---|---|
| Relation | Table | users, orders |
| Tuple | Row / Record | (1, 'alice@…', '2026-01-15') |
| Attribute | Column / Field | id, email, created_at |
| Domain | Data type | INTEGER, VARCHAR(255) |
| Cardinality | Số dòng | 3 (bảng users ở trên) |
| Degree (arity) | Số cột | 3 cột → degree = 3 |
| Schema | Cấu trúc bảng | users(id INT, email VARCHAR, created_at TIMESTAMP) |
| Instance | Tập dòng tại 1 thời điểm | 3 dòng cụ thể trong bảng |
Schema ổn định, ít đổi. Instance đổi liên tục mỗi giây mỗi giờ. Khi thiết kế DB, ta thiết kế schema. Khi vận hành, ta thao tác trên instance.
5. Keys — sáu loại bạn cần phân biệt
5.1. Super key
Tập cột mà kết hợp lại đủ để xác định duy nhất một dòng. Có thể là siêu tập nhiều cột không cần thiết.
Ví dụ bảng users(id, email, name, phone): {id}, {id, email}, {id, email, name}... đều là super keys.
5.2. Candidate key
Super key tối giản — bỏ bất kỳ cột nào thì không còn unique. Ví dụ: {id} và {email} đều có thể là candidate.
5.3. Primary key (PK)
Một candidate key được chọn làm khóa chính. Mỗi bảng chỉ có 1 PK. PK không được NULL, không được trùng.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE
);
5.4. Alternate (Secondary) key
Các candidate key không được chọn làm PK. Trong ví dụ trên, email là alternate key (vẫn UNIQUE).
5.5. Foreign key (FK)
Một (hoặc nhiều) cột tham chiếu PK của bảng khác. Đây là cách mô hình quan hệ "kết nối" các bảng.
CREATE TABLE orders (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
total DECIMAL(10,2) NOT NULL
);
FK đảm bảo referential integrity — không thể có order trỏ đến user_id không tồn tại.
Khi xóa user mà có order trỏ tới? Bạn chọn 1 trong các ON DELETE action:
NO ACTION/RESTRICT— báo lỗi, không cho xóa (mặc định).CASCADE— xóa user → xóa luôn mọi order của user đó.SET NULL— xóa user → orders.user_id = NULL.SET DEFAULT— xóa user → orders.user_id về giá trị default.
5.6. Composite key
PK gồm nhiều cột. Thường thấy ở bảng nối many-to-many:
CREATE TABLE order_items (
order_id BIGINT NOT NULL REFERENCES orders(id),
product_id BIGINT NOT NULL REFERENCES products(id),
qty INT NOT NULL DEFAULT 1,
PRIMARY KEY (order_id, product_id) -- composite PK
);
5.7. Surrogate key vs Natural key
Surrogate key
Khóa nhân tạo do DB sinh — thường BIGSERIAL, UUID. Không có ý nghĩa nghiệp vụ.
Ưu: luôn ổn định (không đổi), gọn (8 byte), nhanh.
Nhược: không tự mô tả; cần JOIN để biết nghĩa.
Natural key
Khóa từ dữ liệu thật — email, số CCCD, mã sản phẩm SKU.
Ưu: có ý nghĩa, dễ debug.
Nhược: có thể đổi (user đổi email!), thường dài/chậm hơn surrogate.
Khuyến nghị thực hành: dùng surrogate key (id BIGSERIAL hoặc UUID v7) cho PK, đồng thời thêm UNIQUE cho natural key (như email).
6. Constraints — luật bất biến của dữ liệu
Constraint là quy tắc mà mọi row trong bảng phải tuân theo. DB tự enforce, app không cần check (và không nên tin app).
6.1. NOT NULL
Cột không được phép NULL.
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL,
name VARCHAR(100) -- có thể NULL
);
6.2. UNIQUE
Đảm bảo mọi dòng có giá trị khác nhau ở cột đó (NULL được coi là khác mọi NULL khác — Postgres mặc định cho phép nhiều NULL).
ALTER TABLE users ADD CONSTRAINT users_email_uniq UNIQUE (email);
-- Composite unique:
ALTER TABLE addresses ADD UNIQUE (user_id, label); -- 1 user có nhiều label, mỗi label duy nhất
6.3. PRIMARY KEY
NOT NULL + UNIQUE + tự tạo index. Mỗi bảng 1 PK duy nhất.
6.4. FOREIGN KEY
Đã trình bày ở 5.5.
6.5. CHECK
Biểu thức boolean phải đúng cho mọi dòng:
CREATE TABLE products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL CHECK (price >= 0),
stock INT NOT NULL DEFAULT 0 CHECK (stock >= 0)
);
-- Check phức tạp:
ALTER TABLE orders ADD CONSTRAINT valid_status
CHECK (status IN ('pending','paid','shipped','delivered','cancelled'));
6.6. DEFAULT
Giá trị tự gán nếu INSERT bỏ qua cột:
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
uuid UUID NOT NULL DEFAULT gen_random_uuid()
6.7. EXCLUSION (Postgres-only, ít gặp)
Tổng quát hóa UNIQUE — đảm bảo không có 2 dòng "xung đột" theo bất kỳ phép so sánh nào, ví dụ không 2 booking trùng khoảng thời gian:
CREATE TABLE bookings (
room_id INT,
during tstzrange,
EXCLUDE USING gist (room_id WITH =, during WITH &&)
);
7. Data types — chọn đúng kiểu, đúng kích cỡ
7.1. Numeric
| Type | Phạm vi | Khi nào dùng |
|---|---|---|
| SMALLINT | −32 768 → 32 767 (2 byte) | Tuổi, level, status code nhỏ |
| INTEGER (INT) | ±2.1 tỷ (4 byte) | ID nhỏ, count thông thường |
| BIGINT | ±9.2 × 10¹⁸ (8 byte) | ID hệ lớn, timestamp ms |
| DECIMAL(p,s) / NUMERIC | Chính xác (variable) | Tiền tệ, ratio cần đúng tuyệt đối |
| REAL / FLOAT4 | ~7 chữ số (4 byte) | Tọa độ, dữ liệu khoa học |
| DOUBLE PRECISION / FLOAT8 | ~15 chữ số (8 byte) | Khoa học, ML feature |
0.1 + 0.2 ≠ 0.3 trong float. Dùng DECIMAL(10,2) hoặc lưu cents dưới dạng INT/BIGINT (số nguyên cents).
7.2. Text
| Type | Mô tả |
|---|---|
| CHAR(n) | Cố định n ký tự, padding bằng space. Hiếm dùng. |
| VARCHAR(n) | Tối đa n ký tự, không pad. Phổ biến. |
| TEXT | Không giới hạn (lên TB). Postgres khuyên dùng TEXT thay VARCHAR khi không cần limit cứng. |
7.3. Date & Time
| DATE | Chỉ ngày: 2026-05-09 |
| TIME | Chỉ giờ: 14:30:00 |
| TIMESTAMP | Ngày + giờ, KHÔNG timezone |
| TIMESTAMPTZ | Ngày + giờ + timezone (lưu UTC, hiển thị theo client tz). Khuyên dùng. |
| INTERVAL | Khoảng thời gian: '3 days', '1 hour 30 minutes' |
7.4. Binary & JSON
- BYTEA — binary blob (file nhỏ, hash). File lớn nên lưu S3, DB chỉ giữ URL.
- JSON / JSONB (Postgres) — JSONB là binary nén + index được + nhanh hơn JSON text.
- UUID — 16 byte, format
550e8400-e29b-41d4-a716-446655440000.
7.5. Special (Postgres)
- ARRAY —
INTEGER[],TEXT[]. Tiện cho tag, list nhỏ. - ENUM — kiểu liệt kê:
CREATE TYPE order_status AS ENUM ('pending','paid','shipped'). - RANGE —
tstzrangecho khoảng thời gian,int4rangecho khoảng số. - GEOMETRY/GEOGRAPHY (PostGIS) — bản đồ, khoảng cách, polygon.
8. NULL — giá trị "không biết"
NULL là một trong những concept khó nhất với người mới. Vì:
- NULL không phải 0, không phải chuỗi rỗng
'', không phải false. - NULL có nghĩa là "chưa biết" hoặc "không áp dụng".
- Mọi phép toán với NULL → kết quả NULL:
NULL + 5 = NULL,NULL = NULL → UNKNOWN(không true!).
8.1. Logic 3 giá trị
SQL không phải 2-valued (TRUE/FALSE) mà là 3-valued: TRUE / FALSE / UNKNOWN.
| x | x = NULL | x IS NULL | NOT (x = NULL) |
|---|---|---|---|
| 5 | UNKNOWN | FALSE | UNKNOWN |
| NULL | UNKNOWN | TRUE | UNKNOWN |
Cẩn thận: WHERE col != 5 sẽ bỏ qua dòng có col = NULL! Vì NULL != 5 → UNKNOWN, không phải TRUE.
-- Lấy users không có email = 'spam@x':
SELECT * FROM users WHERE email != 'spam@x';
-- ❌ Bỏ qua user có email NULL
SELECT * FROM users WHERE email != 'spam@x' OR email IS NULL;
-- ✓ Đầy đủ
8.2. Hàm xử lý NULL
-- COALESCE: trả về tham số đầu tiên không NULL
SELECT COALESCE(nickname, full_name, 'Unknown') FROM users;
-- NULLIF: trả NULL nếu hai tham số bằng nhau
SELECT NULLIF(divisor, 0); -- tránh chia 0
-- IS NULL / IS NOT NULL
WHERE deleted_at IS NULL -- chỉ lấy chưa xóa
8.3. NULL trong aggregate
COUNT(*) đếm cả dòng NULL. COUNT(col) bỏ qua dòng có col NULL. SUM/AVG bỏ qua NULL.
-- Bảng users có 100 dòng, 30 dòng email NULL
SELECT COUNT(*) FROM users; -- 100
SELECT COUNT(email) FROM users; -- 70
SELECT COUNT(DISTINCT email) FROM users; -- số email khác nhau, NULL bị loại
9. Hands-on — cài Postgres và tạo bảng đầu tiên
9.1. Cài Postgres bằng Docker (khuyên dùng)
# File docker-compose.yml
version: '3.8'
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: secret
POSTGRES_DB: learning
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
volumes:
pg_data:
docker compose up -d
docker compose exec db psql -U postgres -d learning
9.2. Tạo bảng đầu tiên
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name VARCHAR(100),
role VARCHAR(20) NOT NULL DEFAULT 'user'
CHECK (role IN ('user','admin','moderator')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
INSERT INTO users (email, name) VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob'),
('carol@example.com', 'Carol');
SELECT * FROM users;
9.3. Thử vi phạm constraint — xem DB chặn lại
-- Trùng email (UNIQUE)
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Alice 2');
-- ERROR: duplicate key value violates unique constraint "users_email_key"
-- NULL ở cột NOT NULL
INSERT INTO users (email) VALUES (NULL);
-- ERROR: null value in column "email" violates not-null constraint
-- CHECK fail
INSERT INTO users (email, role) VALUES ('x@x.com', 'super-admin');
-- ERROR: new row for relation "users" violates check constraint
Đây là DB bảo vệ bạn. Nếu app bug và cố ghi dữ liệu sai, DB từ chối — đảm bảo dữ liệu luôn nhất quán.
10. Bài tập
Bài 1. Cho bảng products(id, sku, name, price, stock, created_at):
- Đặt PK phù hợp.
- SKU phải duy nhất, dạng VARCHAR(50).
- Price không âm, stock không âm.
- created_at tự động lấy thời điểm INSERT.
- Viết CREATE TABLE đầy đủ.
Bài 2. Thiết kế schema cho hệ thống Blog:
- users(id, email, name, password_hash, created_at)
- posts(id, author_id, title, body, published_at, status)
- comments(id, post_id, author_id, body, created_at)
- likes(user_id, post_id, created_at) — composite PK
Viết đầy đủ CREATE TABLE với FK, các constraint hợp lý.
Bài 3. Cho query: SELECT name FROM users WHERE deleted_at != NOW(); — vì sao kết quả luôn rỗng? Sửa lại đúng ý "lấy user chưa bị xóa".
Bài 4. Phân biệt:
- Super key vs Candidate key vs Primary key
- Surrogate key vs Natural key
- UNIQUE vs PRIMARY KEY
Bài 5. ON DELETE thuộc các loại nào? Cho ví dụ thực tế khi nên chọn CASCADE, khi nên chọn RESTRICT, khi nên chọn SET NULL.
11. Quiz — kiểm tra hiểu bài
Quiz cuối Chương 1
Phát biểu nào ĐÚNG về NULL trong SQL?
IS NULL để kiểm tra. COUNT(*) đếm cả dòng có NULL; COUNT(col) mới bỏ qua NULL.Một bảng có thể có:
FK ON DELETE CASCADE nghĩa là:
Vì sao không dùng FLOAT để lưu tiền?
Edgar F. Codd đề xuất mô hình quan hệ năm:
Trong các phát biểu sau, đâu là TRUE về relation (bảng)?
Bạn có bảng users(id, email) với 100 dòng, trong đó 30 dòng email = NULL. Câu nào sai?
email != 'a@x.com' với email NULL trả về UNKNOWN, không phải TRUE. WHERE chỉ giữ TRUE → 30 dòng NULL bị loại. Kết quả là 70, không phải 100. Phải bổ sung OR email IS NULL nếu muốn lấy đầy đủ.Composite primary key:
order_items(order_id, product_id), likes(user_id, post_id). Mỗi tổ hợp duy nhất.Hoàn thành Chương 1. Tiếp theo: Chương 2 — SQL Cơ Bản (DDL/DML/DQL/DCL/TCL) →