Người đến từ MySQL, SQL Server hay Oracle mang theo một trực giác: char(n) cố định nhanh hơn varchar vì độ dài đều đặn, và varchar(n) tiết kiệm hơn text. Cả hai đều sai trong PostgreSQL. Ở đây, text và varchar là cùng một thứ bên trong, còn char(n) không nhanh hơn — nó tốn gấp đôi và gây bẫy. Bài này đo thật ba kiểu chuỗi và giải thích vì sao lời khuyên "dùng char cho cột độ dài cố định" là di sản từ các CSDL khác.

text và varchar: y hệt nhau bên trong

PostgreSQL lưu cả ba kiểu chuỗi bằng cùng một cấu trúc varlena (mảng byte có độ dài biến thiên kèm header). text và varchar dùng cùng cách lưu, cùng toán tử, cùng operator class cho index — không có khác biệt hiệu năng nào:

SELECT 'abc'::text = 'abc'::varchar;         -- t (TRUE)
SELECT pg_typeof('a'::varchar || 'b'::text); -- text

varchar(n) chỉ khác text ở một điểm: nó thêm một ràng buộc CHECK giới hạn độ dài. Không có gì hơn — không tối ưu lưu trữ, không nhanh hơn.

CREATE TEMP TABLE tv(v varchar(5));
INSERT INTO tv VALUES ('chuoi-qua-dai');
-- ERROR: value too long for type character varying(5)

text thì nhận mọi độ dài. Nên lựa chọn giữa text và varchar(n) thuần túy là câu hỏi "bạn có muốn CSDL ép giới hạn độ dài không", không phải câu hỏi hiệu năng.

Ảnh chụp đoạn mã SQL nền tối minh hoạ text vs varchar vs char chọn kiểu chuỗi nào, text và varchar y hệt nhau bên trong cùng cách lưu varlena cùng toán tử cùng opclass cùng hiệu năng SELECT abc text bằng abc varchar t TRUE SELECT pg_typeof a varchar nối b text text varchar n bằng text cộng một ràng buộc CHECK độ dài không hơn, varchar n chỉ thêm giới hạn độ dài CREATE TEMP TABLE tv v varchar 5 INSERT INTO tv VALUES chuoi-qua-dai ERROR value too long for type character varying 5 text không giới hạn nhận mọi độ dài, char n đệm khoảng trắng kẻ gây rối SELECT length abc char 10 bằng 3 cắt space khi đọc SELECT octet_length abc char 10 bằng 10 lưu đủ 10 byte trên đĩa SELECT abc char 10 bằng abc t bỏ space đuôi khi so lưu abc cộng 7 space so sánh và length lại giả vờ không có space, khuyến nghị mặc định text đơn giản không giới hạn giả tạo khi cần varchar n muốn CSDL ép giới hạn độ dài tránh char n đệm space tốn chỗ gây bẫy so sánh

Hình 1: text và varchar dùng chung cách lưu và toán tử; varchar(n) chỉ thêm một CHECK độ dài. char(n) đệm khoảng trắng nên khác biệt và tệ hơn.

Đo thật: char(50) tốn gấp đôi

Trên bảng 5 triệu dòng chứa chuỗi ngắn ('ma123...', 5–9 ký tự), đo kích thước lưu trữ:

Ảnh chụp bảng kết quả đo thật nền tối text vs varchar vs char 5 triệu dòng chuỗi ngắn PostgreSQL 16 giá trị dạng ma123 5 tới 9 ký tự pg_relation_size EXPLAIN ANALYZE shared_buffers 128MB, kích thước lưu trữ text 211 MB lưu đúng độ dài thật varchar 211 MB y hệt text varchar 50 211 MB y hệt 50 chỉ là CHECK char 50 403 MB đệm space tới 50 gấp khoảng 2 lần, tốc độ quét cộng lọc LIKE ma1234 phần trăm text 94 mili giây char 50 94 mili giây nhưng đọc 2 lần dữ liệu từ đĩa khi không cache, char n đệm space gây bẫy length abc char 10 bằng 3 giả vờ không có space octet_length abc char 10 bằng 10 thực tế lưu 10 byte abc char 10 bằng abc bằng t so sánh bỏ space đuôi dữ liệu trên đĩa và giá trị logic không khớp nguồn lỗi âm thầm, varchar n áp ràng buộc text thì không INSERT vào cột varchar 5 chuỗi dài ERROR value too long for type character varying 5 abc text bằng abc varchar bằng t a varchar nối b text text, cốt lõi text và varchar n lưu và chạy y hệt nhau varchar n chỉ thêm một CHECK độ dài mặc định dùng text varchar n khi muốn CSDL ép giới hạn tránh char n đệm space tốn gấp đôi chỗ cho chuỗi ngắn và gây bẫy so sánh length char cố định nhanh hơn là huyền thoại

Hình 2: text, varchar, varchar(50) đều chiếm 211 MB (giống hệt); char(50) chiếm 403 MB — gấp đôi vì đệm khoảng trắng tới 50 ký tự cho mọi giá trị. Tốc độ quét tương đương khi cache, nhưng char đọc gấp đôi dữ liệu từ đĩa.

  • text, varchar, varchar(50): tất cả 211 MB — bằng nhau tuyệt đối, xác nhận chúng lưu y hệt.
  • char(50): 403 MB — gấp đôi. Vì char(n) đệm khoảng trắng mọi giá trị tới đúng n ký tự. Chuỗi 'ma5' (3 ký tự) được lưu thành 'ma5' + 47 khoảng trắng.

Tốc độ quét trong bộ nhớ tương đương (~94 ms cả hai), nhưng đây là khi dữ liệu đã cache. Trên bảng thật lớn hơn RAM, char(50) phải đọc gấp đôi số trang từ đĩa — chậm hơn theo đúng tỉ lệ dung lượng thừa.

char(n): cái bẫy đệm khoảng trắng

Ngoài tốn chỗ, char(n) còn nguy hiểm vì độ dài trên đĩa khác độ dài logic:

SELECT length('abc'::char(10));        -- 3   (cắt space khi đọc)
SELECT octet_length('abc'::char(10));  -- 10  (thực tế LƯU 10 byte)
SELECT 'abc'::char(10) = 'abc';        -- t   (so sánh bỏ space đuôi)

PostgreSQL lưu 'abc' + 7 khoảng trắng trên đĩa, nhưng length() và phép so sánh lại giả vờ các khoảng trắng đó không tồn tại. Sự không nhất quán này là nguồn lỗi âm thầm: dữ liệu bạn ghi ra (ví dụ nối chuỗi, xuất file) mang theo khoảng trắng thừa, trong khi truy vấn so sánh lại bỏ qua chúng. Khi ứng dụng đọc giá trị char(n) và so bằng == ở tầng code (không bỏ space), kết quả lệch với những gì SQL trả về.

Đánh đổi cần cân nhắc

text là mặc định đúng trong PostgreSQL. Không có lý do hiệu năng để chọn varchar(n) thay text. Chỉ dùng varchar(n) khi bạn thật sự muốn CSDL từ chối chuỗi quá dài như một ràng buộc dữ liệu (ví dụ mã bưu chính tối đa 10 ký tự). Ngay cả khi đó, nhiều người thích text + CHECK (length(v) <= 10) để kiểm soát thông báo lỗi rõ hơn.

Đổi varchar(n) sang giới hạn lớn hơn là thao tác nhẹ; thu nhỏ thì không. Tăng n (ví dụ varchar(50) → varchar(100)) chỉ đổi metadata, không viết lại bảng. Nhưng giảm n phải quét kiểm tra mọi dòng. Đây là lý do nữa nên dùng text khi giới hạn chưa chắc chắn.

char(n) chỉ hợp lý cho dữ liệu thật sự cố định độ dài và luôn đủ ký tự — như mã quốc gia char(2) ('VN', 'US') nơi mọi giá trị đúng đủ 2 ký tự, không bao giờ đệm. Nhưng ngay cả ở đây, text cũng chạy tốt như vậy mà không có bẫy khoảng trắng; lợi ích của char(n) gần như bằng không trong PostgreSQL.

Ba ý mang về

  1. text và varchar lưu và chạy y hệt nhau trong PostgreSQL — đo thật cả text, varchar, varchar(50) đều chiếm 211 MB; varchar(n) chỉ thêm một ràng buộc CHECK độ dài, không hơn về hiệu năng hay lưu trữ.
  2. char(n) tốn gấp đôi vì đệm khoảng trắng: đo thật char(50) chiếm 403 MB so với 211 MB của text cho cùng dữ liệu chuỗi ngắn, và đọc gấp đôi từ đĩa khi vượt cache — "char cố định nhanh hơn" là huyền thoại từ CSDL khác.
  3. char(n) còn gây bẫy so sánh: độ dài trên đĩa (octet_length = 10) khác độ dài logic (length = 3), khoảng trắng đuôi bị bỏ khi so sánh nhưng vẫn lưu — mặc định dùng text, varchar(n) khi cần ép giới hạn, tránh char(n).

Phần sau ta đào sâu chi tiết đã chạm ở bài kiểu số: Phần sau đo cách sắp thứ tự cột để giảm padding căn lề — vì sao cùng một bảng, thứ tự cột khác nhau cho kích thước khác nhau, và quy tắc sắp xếp tối ưu.