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

Ảnh chụp đoạn mã SQL nền tối minh hoạ chọn kiểu số cho đúng chính xác kích thước căn lề, float không dùng cho tiền sai số làm tròn thật SELECT 0.1 float8 cộng 0.2 float8 bằng 0.30000000000000004 SELECT 0.1 float8 cộng 0.2 float8 bằng 0.3 f FALSE, SELECT 0.1 numeric cộng 0.2 numeric bằng 0.3 chính xác SELECT bằng 0.3 t TRUE tiền số lượng chính xác dùng numeric đo lường khoa học dùng float, chọn kiểu số nguyên nhỏ nhất vừa đủ smallint int2 2 byte âm 32768 tới 32767 integer int4 4 byte cộng trừ 2,1 tỉ mặc định tốt bigint int8 8 byte cộng trừ 9,2 tỉ tỉ khi thật sự cần int8 tốn gấp đôi int4 chỗ lưu đừng dùng bừa cho id nhỏ, căn lề alignment thứ tự cột ảnh hưởng kích thước PostgreSQL căn int8 theo mốc 8 byte xen kẽ nhỏ lớn phí padding XẤU bool int8 bool int8 bool bool bị đệm 7 byte mỗi cái TỐT int8 int8 bool bool bool gom lớn trước nhỏ dồn cuối cùng cột chỉ khác thứ tự khác dung lượng đáng kể, numeric chính xác nhưng tính chậm hơn SELECT sum v FROM t numeric số học độ chính xác tuỳ ý chậm hơn int8 float8 tính bằng CPU thẳng numeric qua thư viện phần mềm

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:

Ảnh chụp bảng kết quả đo thật nền tối chọn kiểu số 5 triệu dòng PostgreSQL 16 pg_relation_size EXPLAIN ANALYZE shared_buffers 128MB, float vs numeric sai số làm tròn 0.1 cộng 0.2 float8 bằng 0.30000000000000004 bằng 0.3 f FALSE 0.1 cộng 0.2 numeric bằng 0.3 bằng 0.3 t TRUE tiền tệ tuyệt đối không dùng float dùng numeric, kích thước lưu trữ bảng 4 cột 5 triệu dòng 4 nhân int2 smallint 173 MB 2 byte mỗi cột 4 nhân int8 bigint 288 MB 8 byte mỗi cột lớn hơn 66 phần trăm, căn lề cùng cột khác thứ tự 5 cột 5 triệu dòng xen kẽ bool int8 bool int8 bool 326 MB gom nhóm int8 int8 bool bool bool 249 MB chênh 77 MB khoảng 31 phần trăm chỉ vì thứ tự cột padding căn lề bool xen giữa int8, tốc độ tổng hợp SUM 5 triệu dòng int8 123 mili giây float8 120 mili giây numeric 12 phẩy 2 182 mili giây numeric chậm hơn khoảng 50 phần trăm số học độ chính xác tuỳ ý chạy bằng phần mềm không phải CPU thẳng, cốt lõi numeric cho tiền chính xác chậm hơn float cho đo lường khoa học nhanh có sai số chọn int nhỏ nhất vừa đủ int8 tốn gấp đôi int4 gom cột theo kích thước giảm padding căn lề

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ỗi bool trước int8 bị đệm tới 7 byte.
  • Gom nhóm (int8, int8, bool, bool, bool): 249 MB — các int8 liền nhau không cần đệm, ba bool dồ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ề

  1. float không dùng cho tiền — đo thật 0.1 + 0.2 trong float8 cho 0.30000000000000004 khác 0.3; dùng numeric cho tiền và số lượng cần chính xác tuyệt đối, float chỉ cho đo lường khoa học chấp nhận sai số.
  2. Chọn kiểu int nhỏ nhất vừa đủ: đo thật bảng 4 cột int8 chiếm 288 MB so với 173 MB của int2 (lớn hơn 66%), và numeric tính SUM chậm hơn ~50% so với int8/float8 vì dùng số học phần mềm thay vì CPU trực tiếp.
  3. Thứ tự cột ảnh hưởng kích thước vì căn lề: cùng 5 cột, xếp bool xen int8 chiếm 326 MB còn gom int8 trước rồi bool sau 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.