Chọn kiểu số cho một cột có vẻ là quyết định tầm thường — cứ bigint cho chắc, float cho số thập phân, xong. Nhưng những lựa chọn "cho chắc" đó âm thầm tốn dung lượng, làm chậm truy vấn, và tệ nhất là gây sai số tiền bạc. Bài này đo thật ba khía cạnh: vì sao float là thảm họa cho tiền, int8 tốn gấp đôi int4 ra sao, và một điều ít ai biết — thứ tự cột ảnh hưởng kích thước bảng vì căn lề bộ nhớ.
float không dùng cho tiền: sai số làm tròn
Điều quan trọng nhất trước: float4/float8 là số dấu phẩy động IEEE 754, không lưu chính xác phần lớn số thập phân. Kinh điển:
SELECT 0.1::float8 + 0.2::float8; -- 0.30000000000000004
SELECT (0.1::float8 + 0.2::float8) = 0.3; -- f (FALSE!)
SELECT (0.1::numeric + 0.2::numeric) = 0.3; -- t (TRUE)
0.1 + 0.2 trong float cho 0.30000000000000004, không bằng 0.3. Với tiền tệ, sai số này tích lũy và gây lệch sổ sách — một hóa đơn cộng dồn hàng nghìn dòng có thể lệch vài xu, đủ để kiểm toán không khớp. Kiểu đúng cho tiền là numeric (còn gọi decimal) — nó lưu chính xác đến từng chữ số.

Hình 1: float gây sai số làm tròn nên không dùng cho tiền — dùng numeric. Chọn kiểu int nhỏ nhất vừa đủ. Thứ tự cột ảnh hưởng kích thước vì căn lề bộ nhớ.
Kích thước và tốc độ: đo thật
Trên bảng 5 triệu dòng, đo cả kích thước lưu trữ lẫn tốc độ tổng hợp:

Hình 2: Bảng 4 cột int2 chiếm 173 MB, cùng bảng dùng int8 chiếm 288 MB (lớn hơn 66%). Cùng bộ cột, thứ tự bool xen int8 chiếm 326 MB còn gom nhóm chỉ 249 MB — chênh 77 MB. numeric SUM 182 ms so với 123 ms của int8.
Kích thước theo kiểu int (bảng 4 cột, 5 triệu dòng): int2 chiếm 173 MB, int8 chiếm 288 MB — lớn hơn 66%. int8 mỗi cột 8 byte so với int2 2 byte. Với bảng lớn, dùng bigint cho một cột chỉ chứa số nhỏ (mã trạng thái, tuổi, số lượng) là lãng phí thẳng dung lượng và băng thông I/O.
Tốc độ SUM (5 triệu dòng): int8 123 ms, float8 120 ms, numeric 182 ms — chậm hơn ~50%. int8/float8 được CPU tính trực tiếp bằng lệnh phần cứng; numeric là số học độ chính xác tùy ý chạy bằng thư viện phần mềm, nên chậm hơn hẳn. Đây là cái giá của tính chính xác.
Căn lề: thứ tự cột ảnh hưởng kích thước
Đây là điều bất ngờ nhất, và ít người biết. PostgreSQL căn lề mỗi cột theo mốc phù hợp kiểu của nó: int8 phải bắt đầu ở địa chỉ chia hết cho 8, int4 chia hết cho 4. Khi một cột nhỏ (bool, 1 byte) đứng ngay trước một int8, PostgreSQL phải chèn byte đệm (padding) để int8 rơi đúng mốc.
Đo thật cùng 5 cột (ba bool, hai int8), chỉ khác thứ tự:
- Xen kẽ
(bool, int8, bool, int8, bool): 326 MB — mỗibooltrướcint8bị đệm tới 7 byte. - Gom nhóm
(int8, int8, bool, bool, bool): 249 MB — cácint8liền nhau không cần đệm, babooldồn cuối.
Chênh 77 MB (~31%) chỉ vì thứ tự cột — cùng dữ liệu y hệt. Quy tắc thực dụng: khai báo cột theo thứ tự kích thước giảm dần (các kiểu 8 byte trước, rồi 4 byte, rồi 2 byte, rồi bool/char 1 byte cuối). PostgreSQL không tự sắp lại; thứ tự bạn viết CREATE TABLE là thứ tự vật lý.
Đánh đổi cần cân nhắc
Đừng tối ưu kích thước int quá sớm khi có rủi ro tràn. int4 đủ cho hầu hết id (tới 2,1 tỉ), nhưng nếu bảng có thể vượt mức đó (log sự kiện, bảng nối lớn), dùng int8 ngay từ đầu — đổi kiểu cột trên bảng tỉ dòng là thao tác khóa bảng đau đớn. Cân giữa tiết kiệm và rủi ro tràn theo tăng trưởng thực tế.
numeric chậm hơn nhưng đừng vì thế bỏ nó cho tiền. 50% chậm hơn nghe nhiều, nhưng với tiền tệ tính đúng quan trọng hơn tốc độ — một truy vấn báo cáo chậm thêm 60 ms không đáng lo bằng sổ sách lệch. Chỉ tránh numeric khi bạn không cần chính xác tuyệt đối (thống kê xấp xỉ, tọa độ, đo lường).
Căn lề đáng quan tâm ở bảng lớn, không phải bảng nhỏ. Sắp cột theo kích thước là thói quen tốt, nhưng đừng hy sinh tính dễ đọc của schema cho một bảng vài nghìn dòng. Với bảng hàng trăm triệu dòng thì 31% dung lượng là thật và đáng sắp lại; với bảng nhỏ thì ưu tiên nhóm cột theo logic nghiệp vụ.
Ba ý mang về
floatkhông dùng cho tiền — đo thật0.1 + 0.2trongfloat8cho0.30000000000000004khác0.3; dùngnumericcho tiền và số lượng cần chính xác tuyệt đối,floatchỉ cho đo lường khoa học chấp nhận sai số.- Chọn kiểu int nhỏ nhất vừa đủ: đo thật bảng 4 cột
int8chiếm 288 MB so với 173 MB củaint2(lớn hơn 66%), vànumerictính SUM chậm hơn ~50% so vớiint8/float8vì dùng số học phần mềm thay vì CPU trực tiếp. - Thứ tự cột ảnh hưởng kích thước vì căn lề: cùng 5 cột, xếp
boolxenint8chiếm 326 MB còn gomint8trước rồiboolsau chỉ 249 MB (chênh 77 MB) — khai báo cột theo kích thước giảm dần để giảm byte đệm.
Phần sau ta xét lựa chọn kiểu tương tự cho chuỗi: Phần sau đo text so với varchar(n) so với char(n) — vì sao trong PostgreSQL chúng gần như không khác nhau về hiệu năng, và khi nào char(n) thật sự tệ hơn.