EAV (entity-attribute-value) lưu mỗi thuộc tính thành một dòng:
create table custom_values (
entity_id bigint not null,
attribute_id bigint not null references custom_attributes(id),
value_text text,
value_number numeric,
primary key (entity_id, attribute_id)
);Ưu: có thể mô tả và ràng buộc từng thuộc tính (kiểu dữ liệu, bắt buộc, danh sách giá trị) ngay trong database, và query "tìm mọi bản ghi có thuộc tính X" là một lần tra index. Nhược: đọc một bản ghi có n trường cần n dòng rồi pivot lại, kiểu dữ liệu bị làm phẳng thành text, query nhiều điều kiện phải self-join nhiều lần — plan xấu rất nhanh.
JSONB gom tất cả vào một cột:
alter table profiles add column custom jsonb not null default '{}';
create index on profiles using gin (custom jsonb_path_ops);Ưu: đọc/ghi cả bản ghi trong một lần, không join, index GIN vẫn lọc được theo khoá bên trong. Nhược: database không kiểm tra kiểu hay ràng buộc bên trong JSON (ứng dụng phải tự validate), thống kê của planner cho cột JSON kém chính xác nên ước lượng số dòng dễ sai, và sửa một khoá là ghi lại cả document.
Chọn thế nào: phần lớn trường hợp SaaS ngày nay chọn JSONB cho trường tuỳ biến vì đơn giản hơn hẳn, kèm một bảng custom_field_definitions mô tả schema để tầng ứng dụng validate và dựng form. EAV chỉ còn hợp lý khi cần metadata phong phú cho từng thuộc tính và số lượng thuộc tính rất lớn nhưng mỗi bản ghi chỉ dùng vài cái (dữ liệu thưa).
Điểm quan trọng nhất khi trả lời: đừng đưa trường lõi vào chỗ động. Những cột hệ thống truy vấn, join hay ràng buộc thường xuyên phải là cột thật; chỉ những gì khách tự định nghĩa mới nằm trong JSONB/EAV.