jsonb là một trong những tính năng được yêu thích nhất của PostgreSQL — nó cho phép lưu dữ liệu có cấu trúc linh hoạt mà không cần khai báo lược đồ cứng. Cám dỗ rất lớn: "cứ nhét mọi thứ vào một cột jsonb, khỏi phải ALTER TABLE mỗi lần thêm trường". Nhưng sự linh hoạt đó không miễn phí. Bài này đo thật cái giá của jsonb so với cột riêng — dung lượng, tốc độ, kích thước index — và chỉ ra khi nào mỗi lựa chọn là đúng.
Hai cách lưu cùng một dữ liệu
Cùng bốn trường (ten, tuoi, thanh_pho, active), hai thiết kế:
-- cột riêng: mỗi trường một cột có kiểu
CREATE TABLE c_col (id int8, ten text, tuoi int4, thanh_pho text, active bool);
-- jsonb: gom mọi trường vào một cột linh hoạt
CREATE TABLE c_json (id int8, data jsonb);
-- data = {"ten":"user1","tuoi":25,"thanh_pho":"HCM","active":true}
Điểm mấu chốt về lưu trữ: jsonb lặp tên khoá ở mỗi dòng. Cột riêng lưu tên cột một lần trong catalog; jsonb lưu chuỗi "thanh_pho", "tuoi"... trong từng bản ghi. Với 3 triệu dòng, đó là 3 triệu lần lặp mỗi tên khoá.

Hình 1: Cột riêng dùng kiểu sẵn có; jsonb trích trường bằng ->> rồi ép kiểu mỗi lần, hoặc @> cho containment. Index jsonb có hai lựa chọn: biểu thức (nhanh, cứng) hoặc GIN (linh hoạt, lớn).
Đo thật: dung lượng, tốc độ, index
Trên 3 triệu dòng, đo cả ba khía cạnh:

Hình 2: Cột riêng 172 MB so với jsonb 357 MB (~2×). Lọc thanh_pho='HCM' AND tuoi>50: cột riêng 51 ms, jsonb không index 127 ms, jsonb index biểu thức 70 ms, jsonb GIN 119 ms. Index GIN jsonb 225 MB so với btree cột 20 MB.
- Dung lượng: cột riêng 172 MB so với jsonb 357 MB — gấp ~2 lần, do lặp tên khoá.
- Tốc độ lọc
thanh_pho='HCM' AND tuoi>50:- Cột riêng + btree: 51 ms.
- jsonb không index:
Seq Scan, 127 ms (phải trích->>cho mỗi dòng). - jsonb + index biểu thức: 70 ms — chậm hơn cột riêng.
- jsonb + GIN,
data @> '{...}': 119 ms — linh hoạt nhưng chậm.
- Kích thước index: btree cột 20 MB so với GIN jsonb 225 MB — gấp ~11 lần.
Cột riêng thắng trên mọi trục khi lược đồ đã biết: nhỏ hơn, nhanh hơn, index gọn hơn nhiều.
Index cho jsonb: hai lựa chọn, hai đánh đổi
Nếu dùng jsonb và cần truy vấn theo trường, có hai cách index:
Index biểu thức trên trường cụ thể: CREATE INDEX ON c_json ((data->>'thanh_pho')). Nhanh (70 ms, gần cột riêng) nhưng bạn phải biết trước truy vấn theo trường nào — đúng cái linh hoạt mà jsonb hứa hẹn bị mất. Mỗi trường muốn index nhanh phải khai riêng.
Index GIN trên cả cột: CREATE INDEX ON c_json USING gin(data). Linh hoạt — hỗ trợ truy vấn containment @> trên bất kỳ trường mà không cần khai trước. Nhưng nó lớn (225 MB, gấp 11 lần) và chậm hơn (119 ms). Đây là cái giá thật của "truy vấn mọi trường linh hoạt".
Đánh đổi cần cân nhắc
Cột riêng cho trường đã biết, ổn định, hay truy vấn. Nếu bạn biết cấu trúc dữ liệu và sẽ lọc/sắp theo các trường đó thường xuyên, cột riêng là lựa chọn đúng: nhanh, gọn, và có kiểu + ràng buộc (NOT NULL, CHECK, khóa ngoại) mà jsonb không ép được. tuoi int4 từ chối chuỗi rác; data->>'tuoi' thì nhận mọi thứ.
JSONB cho dữ liệu thật sự biến thiên hoặc thưa thớt. Khi mỗi bản ghi có tập trường khác nhau (thuộc tính sản phẩm đa dạng, cấu hình tùy biến, payload webhook), hoặc trường xuất hiện thưa (99% dòng không có), jsonb tránh được một bảng đầy cột NULL hoặc hàng chục ALTER TABLE. Đây là chỗ nó tỏa sáng — linh hoạt là tính năng, không phải khuyết điểm, khi dữ liệu vốn không có lược đồ cố định.
Mô hình lai thường là tốt nhất. Thực tế phổ biến: đặt các trường lõi hay truy vấn thành cột riêng (id, ngày tạo, trạng thái, khóa ngoại), và một cột jsonb tên extra/metadata cho phần biến thiên. Bạn được tốc độ và ràng buộc cho phần quan trọng, và linh hoạt cho phần đuôi dài. Đừng coi đây là lựa chọn nhị phân toàn-hoặc-không.
Ba ý mang về
- JSONB tốn gấp đôi dung lượng vì lặp tên khoá mỗi dòng: đo thật cùng dữ liệu, cột riêng 172 MB so với jsonb 357 MB, và index GIN jsonb 225 MB so với btree cột 20 MB (gấp ~11 lần) — linh hoạt có giá thật về lưu trữ.
- Cột riêng truy vấn nhanh hơn và có kiểu/ràng buộc: đo thật lọc hai trường chạy 51 ms với cột riêng so với 70–127 ms của jsonb; cột riêng ép được
NOT NULL,CHECK, khóa ngoại mà jsonb không làm được. - Chọn theo tính biến thiên của dữ liệu, và cân nhắc mô hình lai: cột riêng cho trường ổn định hay truy vấn, jsonb cho trường thật sự thay đổi/thưa thớt theo dòng — thực tế hay dùng cột lõi + một cột
jsonbextra để được cả hai.
Phần sau ta xét một lựa chọn thiết kế tương tự cho dữ liệu nhiều giá trị: Phần sau đo kiểu mảng của PostgreSQL so với bảng con chuẩn hóa — khi nào một cột mảng gọn và nhanh, khi nào bảng con với khóa ngoại là đúng.