Giao diện
2. Kiểu dữ liệu
Hiệu năng bắt đầu từ việc chọn đúng kiểu cho từng cột. Mỗi byte thừa bị nhân lên theo số dòng, rồi nhân tiếp theo số index chứa cột đó.
Vì sao kiểu dữ liệu quyết định hiệu năng
Khi hệ thống lớn dần, chi phí của kiểu dữ liệu “hơi to” lộ ra ở ba chỗ:
- Disk: dữ liệu to hơn thì phải đọc ghi nhiều page hơn; backup, restore và replication cũng chậm hơn.
- RAM: InnoDB giữ page dữ liệu và index trong buffer pool (xem chương 1). Dữ liệu nhỏ thì nhiều dòng nằm vừa RAM hơn. Khi phần dữ liệu hay được truy cập (working set) vượt quá buffer pool, đọc RAM chuyển thành đọc disk và latency nhảy bậc.
- CPU: so sánh số nguyên rẻ hơn nhiều so với so sánh chuỗi, vì chuỗi phải so theo collation.
Ví dụ: cột status lưu bằng INT (4 byte) thay vì TINYINT (1 byte). Với 1 tỷ dòng, phần thừa là 3 GB, chưa tính các index có chứa cột này.
Nguyên tắc chọn kiểu
- Hiểu phạm vi dữ liệu. Có bao nhiêu trạng thái? Vài chục thì
TINYINTlà đủ. ID có thể vượt 2,1 tỷ (INT) hoặc 4,29 tỷ (INT UNSIGNED) không? Nếu có, dùngBIGINTngay từ đầu, vì đổi kiểu cột khoá trên bảng lớn rất tốn kém. - Ưu tiên số nguyên hơn chuỗi cho ID, trạng thái, mã loại. Index trên số nhỏ hơn và so sánh nhanh hơn. Số điện thoại, email vẫn phải là chuỗi (số 0 ở đầu, dấu +), nhưng index trên chúng sẽ to hơn.
- Đơn giản và đúng bản chất. Thời gian dùng
DATE/DATETIME, không lưu chuỗi'20261013': chuỗi mất kiểm tra hợp lệ, không dùng được hàm ngày tháng, so sánh theo collation. Hash dùngBINARY. - NOT NULL khi nghiệp vụ không có khái niệm “chưa biết”, kèm
DEFAULThợp lý. Lý do chính là tính đúng đắn của query, không phải dung lượng.
NULL: tốn bao nhiêu và bẫy ở đâu
Với row format COMPACT/DYNAMIC (mặc định), InnoDB dùng một NULL bitmap ở đầu mỗi dòng: mỗi cột cho phép NULL tốn 1 bit, làm tròn lên byte. Bảng có 8 cột nullable chỉ tốn thêm 1 byte mỗi dòng, và giá trị NULL không chiếm chỗ ở phần dữ liệu. Index trên cột nullable vẫn dùng bình thường; WHERE col IS NULL cũng dùng được index (EXPLAIN type ref_or_null).
Cái giá thật của NULL là logic ba giá trị (TRUE / FALSE / UNKNOWN), dễ làm query trả sai mà không báo lỗi:
WHERE status != 2bỏ qua các dòng có status NULL.WHERE id NOT IN (SELECT ref_id FROM …)trả về rỗng nếu subquery có dù chỉ một NULL. DùngNOT EXISTS.- UNIQUE cho phép nhiều dòng cùng NULL.
COUNT(col),SUM,AVGbỏ qua NULL.
Số nguyên, số thực, tiền
TINYINT/SMALLINT/MEDIUMINT/INT/BIGINT: 1 / 2 / 3 / 4 / 8 byte. Chọn theo mức mở rộng dự kiến.UNSIGNEDdời phạm vi sang không âm, nên giá trị tối đa gấp đôi. Bẫy: phép trừ cho ra số âm sẽ báo lỗi BIGINT UNSIGNED value is out of range.- Số trong ngoặc như
INT(11)chỉ là độ rộng hiển thị, không đổi dung lượng hay phạm vi, đã deprecated từ 8.0.17.BOOLEANchính làTINYINT(1). FLOAT/DOUBLEnhanh nhưng xấp xỉ (0,1 + 0,2 ≠ 0,3). Không bao giờ dùng cho tiền.DECIMAL(p,s)chính xác tuyệt đối, khoảng 4 byte cho mỗi 9 chữ số.- Tiền: VNĐ không có phần lẻ nên
BIGINTlà lựa chọn gọn và nhanh. Hệ đa tiền tệ: lưu đơn vị nhỏ nhất (xu, cent) bằngBIGINTkèm cột mã tiền tệ, hoặcDECIMAL(19,4).
Chuỗi
- Charset: dùng
utf8mb4. Trong MySQL,utf8là utf8mb3 (tối đa 3 byte), không lưu được emoji. - VARCHAR(n): độ dài thay đổi, tốn độ dài thật cộng 1 byte (nếu tối đa ≤ 255 byte) hoặc 2 byte. Đặt n vừa đủ, đừng mặc định 255: bảng tạm và sort buffer có thể cấp phát theo độ dài tối đa. Khoá index tối đa 3072 byte, tức
VARCHAR(768)với utf8mb4. - CHAR(n): chỉ thật sự cố định độ dài khi charset 1 byte (latin1, ascii). Với utf8mb4, InnoDB lưu
CHAR(n)như kiểu thay đổi độ dài (từ n tới 4n byte). Dùng cho mã ngắn, đều nhau như mã quốc giaCHAR(2). - Hash, UUID: lưu nhị phân. MD5 vào
BINARY(16)quaUNHEX(MD5(…)): 16 byte thay vì 32 ký tự, so sánh nhị phân nhanh hơn. - ENUM: gọn (1–2 byte) nhưng có nhược điểm: sắp xếp theo số thứ tự nội bộ chứ không theo chữ; thêm giá trị vào giữa danh sách phải rebuild bảng (thêm vào cuối thì INSTANT); so sánh với số dễ nhầm sang chỉ số. Nhiều team dùng
TINYINTkèm hằng số trong code, hoặc bảng lookup. - TEXT / BLOB / JSON: giá trị dài nằm ở page tràn (off-page). Không SELECT các cột này khi không cần; bảng tạm có cột TEXT dễ phải xuống disk.
Thời gian
- TIMESTAMP (4 byte): khi lưu, đổi từ
time_zonecủa session sang UTC; khi đọc, đổi ngược lại. Phạm vi 1970-01-01 00:00:01 tới 2038-01-19 03:14:07 UTC. - DATETIME (5 byte): lưu nguyên giá trị, không đổi múi giờ, phạm vi năm 1000 tới 9999.
- Cả hai thêm 0–3 byte nếu có phần lẻ giây (
DATETIME(3)cho mili giây).
Chênh lệch 1 byte không đáng kể. Khác biệt quan trọng là múi giờ và giới hạn năm 2038: ngày hết hạn hợp đồng 15 năm hay ngày đáo hạn khoản vay dài hạn đã vượt mốc này. Cách làm phổ biến: DATETIME và quy ước app luôn ghi giờ UTC.
Đồng hồ 2038
Còn … tới khi cột TIMESTAMP không lưu được thời điểm mới nữa.
Kiểu cho ID và khoá chính
ID xuất hiện ở mọi nơi: join, điều kiện WHERE, khoá ngoại. Trong InnoDB, khoá chính còn bị chép vào mọi secondary index (xem chương Index). Chọn sai kiểu ID làm phình tất cả index, join chậm và buffer pool kém hiệu quả.
- Dùng
INT UNSIGNEDhoặcBIGINTtự tăng. - Cột khoá ngoại cùng kiểu với cột được tham chiếu, kể cả dấu UNSIGNED và charset/collation nếu là chuỗi.
- Tránh UUID dạng chuỗi
CHAR(36): 36 byte, so sánh theo collation. - Cần UUID (sinh ID ở client, gộp dữ liệu nhiều nguồn) thì lưu
BINARY(16)và chọn loại có thứ tự: UUIDv7 (phần đầu là timestamp) hoặcUUID_TO_BIN(uuid, 1)với UUIDv1. UUIDv4 ngẫu nhiên khiến mỗi insert rơi vào một page bất kỳ trong clustered index: page split, phân mảnh, page chỉ lấp khoảng 69%. - Hệ phân tán cần ID tăng dần theo thời gian: ID kiểu Snowflake (timestamp + máy + số thứ tự) vừa với
BIGINT.
Thử: dung lượng theo kiểu khoá chính
Ước tính thô: mỗi dòng clustered tốn thêm 18 byte (header 5, trx_id 6, roll_ptr 7); mỗi entry secondary = header 5 + khoá 8 + PK. Page lấp 15/16 khi insert tăng dần, khoảng 69% khi insert ngẫu nhiên.
Bảng (clustered index)Các secondary index
Với 500 triệu dòng và 4 secondary index, CHAR(36) UUIDv4 tốn khoảng 231 GB, so với 123 GB của BIGINT (gấp 1,9 lần). Một phần do chuỗi dài hơn và bị lặp trong mọi index, phần còn lại do insert ngẫu nhiên làm page chỉ lấp khoảng 69%. UUIDv7 dạng BINARY(16) giữ được thứ tự tăng dần: 144 GB.
Bảng tra nhanh
| Kiểu | Byte | Phạm vi / ghi chú |
|---|---|---|
TINYINT | 1 | −128..127, UNSIGNED 0..255. Trạng thái, cờ |
SMALLINT | 2 | ±32.767, UNSIGNED 0..65.535 |
INT | 4 | ±2,1 tỷ, UNSIGNED 0..4,29 tỷ |
BIGINT | 8 | ±9,2×10¹⁸. ID lớn, tiền theo đơn vị nhỏ nhất |
DECIMAL(p,s) | ~4 / 9 chữ số | Chính xác tuyệt đối |
FLOAT / DOUBLE | 4 / 8 | Xấp xỉ |
DATE | 3 | 1000-01-01..9999-12-31 |
DATETIME(fsp) | 5 + 0..3 | Không đổi múi giờ |
TIMESTAMP(fsp) | 4 + 0..3 | Đổi sang UTC; tới 2038-01-19 03:14:07 UTC |
CHAR(n) utf8mb4 | n..4n | Mã ngắn, đều độ dài |
VARCHAR(n) | độ dài + 1 hoặc 2 | Đặt n vừa đủ |
BINARY(16) | 16 | UUID, MD5 dạng nhị phân |
TEXT / BLOB / JSON | tuỳ | Off-page; tránh SELECT khi không cần |
Ghi nhớ
- Nhỏ nhất mà vẫn đủ cho tương lai gần.
- Đúng bản chất: số là số, thời gian là thời gian, hash là nhị phân.
- NOT NULL khi nghiệp vụ không có “chưa biết”; cẩn thận
NOT INvà!=với NULL. - Khoá chính nhỏ và tăng dần, vì nó nằm trong mọi index.