Mở đầu — vì sao bài này quan trọng
Trong suốt khóa học, chúng ta đã rèn luyện một tư duy gần như "kinh điển": mỗi thông tin là một cột (column), mỗi cột có một kiểu dữ liệu rõ ràng, và mọi thứ phải tuân theo các quy tắc chuẩn hóa (normalization) chặt chẽ. Đó là nền tảng vững chắc, và bạn nên giữ lấy nó. Nhưng nếu bạn đã từng làm việc với một hệ thống thực tế ở Việt Nam — một app gọi xe, một sàn thương mại điện tử, một nền tảng đặt lịch khám bệnh — bạn sẽ sớm gặp những dữ liệu "không chịu nằm yên" trong cái khuôn bảng cột truyền thống.
Ví dụ: cấu hình của một sản phẩm điện thoại có 12 thuộc tính, nhưng một cái áo thun chỉ có 3 thuộc tính (màu, size, chất liệu), còn một gói bảo hiểm lại có cấu trúc hoàn toàn khác. Nếu bạn cố nhồi tất cả vào một bảng phẳng, bạn sẽ có hàng chục cột để trống (sparse columns) — vừa lãng phí, vừa khó bảo trì.
Đây chính là lúc JSON và dữ liệu bán cấu trúc (semi-structured data) bước vào. Với vai trò là một Business Analyst làm về data modeling, bạn không cần tự tay viết code, nhưng bạn bắt buộc phải biết khi nào nên đề xuất một cột JSON, khi nào thì tuyệt đối không, và làm sao để model loại dữ liệu này một cách có trách nhiệm. Hiểu sai chỗ này, bạn có thể biến một database sạch sẽ thành một "bãi rác linh hoạt" mà không ai dám đụng vào sau 2 năm.
Khái niệm cốt lõi
Dữ liệu bán cấu trúc là gì?
Dữ liệu có ba dạng chính. Dữ liệu có cấu trúc (structured) là các bảng quan hệ với schema cố định — điều bạn đã quen. Dữ liệu phi cấu trúc (unstructured) là ảnh, video, file PDF — không có khuôn mẫu nào để máy đọc trực tiếp. Ở giữa là dữ liệu bán cấu trúc: nó có cấu trúc (các cặp khóa–giá trị, danh sách, lồng nhau), nhưng cấu trúc đó linh hoạt — không bắt buộc mọi bản ghi phải giống nhau.
JSON (JavaScript Object Notation) là định dạng phổ biến nhất của dữ liệu bán cấu trúc. Một đối tượng JSON trông như sau:
{
"mau_sac": "Đen",
"dung_luong": "256GB",
"bao_hanh_thang": 12,
"phu_kien_kem": ["sạc", "cáp", "ốp lưng"]
}
Điều quan trọng: bản ghi tiếp theo có thể có khóa hoàn toàn khác, ví dụ một cái áo chỉ có {"mau_sac": "Trắng", "size": "L"}. Đó là sức mạnh — và cũng là cạm bẫy — của linh hoạt.
JSON column trong cơ sở dữ liệu
Hầu hết các DBMS hiện đại đều hỗ trợ kiểu dữ liệu JSON như một cột bình thường trong bảng quan hệ:
- MySQL/MariaDB (rất phổ biến ở các dự án web Việt Nam): kiểu
JSON, có hàmJSON_EXTRACT,->,->>. - PostgreSQL: hai kiểu
jsonvàjsonb—jsonblưu dạng nhị phân, đánh index được, nhanh hơn nhiều khi truy vấn. - SQL Server: lưu JSON trong cột
NVARCHARkèm các hàmJSON_VALUE,OPENJSON.
products vẫn có các cột chuẩn — id, name, price, created_at — và thêm một cột attributes JSON để chứa phần thuộc tính biến đổi.Bốn tình huống nên cân nhắc dùng JSON column
Đây là phần cốt lõi của bài này, đúng theo bốn tiêu chí kinh điển:
- Schema linh hoạt / đang tiến hóa (flexible/evolving): Khi bạn chưa biết chắc cấu trúc cuối cùng, hoặc cấu trúc sẽ thay đổi liên tục mà bạn không muốn chạy migration (ALTER TABLE) mỗi lần.
- Thuộc tính biến đổi theo từng bản ghi (variable attributes per record): Mỗi loại sản phẩm, mỗi loại sự kiện có tập thuộc tính khác nhau. Đây là tình huống "Entity–Attribute–Value" mà JSON xử lý gọn hơn nhiều.
- Dữ liệu lồng nhau (nested data): Khi thông tin tự nhiên có dạng cây — một đơn hàng có nhiều dòng hàng, mỗi dòng hàng có nhiều tùy chọn — và bạn muốn lưu nguyên khối để đọc/ghi cùng lúc.
- Tránh quá nhiều cột rỗng (avoiding many sparse columns): Khi nếu trải phẳng ra, bạn sẽ có một bảng 80 cột mà mỗi bản ghi chỉ dùng 5–10 cột, phần còn lại toàn
NULL.
Cái giá phải trả
Linh hoạt không miễn phí. Khi đưa dữ liệu vào JSON, bạn đánh đổi:
- Mất kiểm tra ràng buộc tự động: Database không thể bắt buộc kiểu dữ liệu,
NOT NULL, hay khóa ngoại bên trong JSON. Nếu một dev ghi"bao_hanh_thang": "mười hai"thay vì số12, database vẫn chấp nhận. - Truy vấn và báo cáo khó hơn: Lọc, tổng hợp (aggregate), join trên trường JSON phức tạp và thường chậm hơn cột thường.
- Khó tài liệu hóa: Đây là điều bạn quan tâm với tư cách BA — một cột JSON là một "hộp đen" nếu không có data dictionary đi kèm.
Tình huống thực tế
Tình huống 1 — Sàn TMĐT đa ngành hàng (kiểu Tiki / Shopee)
Một sàn thương mại điện tử Việt Nam bán mọi thứ: điện thoại, thời trang, thực phẩm, đồ gia dụng. Đội kỹ thuật ban đầu thiết kế bảng products với mọi thuộc tính có thể nghĩ ra: ram, storage, screen_size, material, expiry_date, wattage... Sau 6 tháng, bảng phình lên 64 cột, và 90% trong số đó là NULL với bất kỳ sản phẩm nào. Báo cáo của team data cho thấy một sản phẩm sách trung bình chỉ dùng 4 cột.
Giải pháp được nhóm BA đề xuất: giữ các cột "xương sống" dùng chung cho mọi sản phẩm (id, name, price, category_id, brand, stock), và gom toàn bộ thuộc tính đặc thù theo ngành hàng vào một cột specs JSON. Điện thoại thì {"ram": "8GB", "storage": "256GB"}, thực phẩm thì {"han_su_dung": "2026-12-31", "khoi_luong": "500g"}.
Bài học rút ra: JSON đã xóa sạch hàng chục cột rỗng. Nhưng nhóm cũng quyết định: thuộc tính brand và category không nằm trong JSON, vì người dùng lọc theo hãng và danh mục liên tục — những trường này phải là cột thật, có index. Ranh giới "lọc thường xuyên → cột thật" được vẽ rất rõ.
Tình huống 2 — App giao đồ ăn lưu snapshot đơn hàng (kiểu GrabFood / Baemin)
Một app giao đồ ăn gặp vấn đề kinh điển: khi khách đặt một tô phở với tùy chọn "thêm trứng, ít hành, bánh phở to", và 3 tháng sau nhà hàng đổi giá hoặc bỏ món, đơn hàng cũ vẫn phải hiển thị đúng như lúc đặt. Nếu họ join sang bảng menu_items hiện tại, dữ liệu lịch sử sẽ bị sai khi menu thay đổi.
Đội thiết kế quyết định lưu một snapshot của từng dòng đơn hàng dưới dạng JSON ngay tại thời điểm đặt:
{
"ten_mon": "Phở bò tái",
"gia_luc_dat": 65000,
"tuy_chon": [
{"ten": "Thêm trứng", "gia": 10000},
{"ten": "Ít hành", "gia": 0}
]
}
Đây là ví dụ điển hình của dữ liệu lồng nhau và bất biến theo thời gian. Snapshot này không bao giờ cần join hay sửa; nó chỉ đọc lại nguyên khối khi khách xem lịch sử đơn.
Bài học rút ra: JSON tuyệt vời cho dữ liệu "đóng băng" theo thời điểm. Tuy nhiên team vẫn lưu total_amount (tổng tiền) thành cột thật ở cấp đơn hàng, vì kế toán cần SUM doanh thu hằng ngày — và bạn không muốn bắt hệ thống bóc tách JSON của hàng triệu đơn để cộng tiền.
Tình huống 3 — Nền tảng bảo hiểm với sản phẩm đa dạng
Một công ty insurtech ở TP.HCM bán nhiều loại hợp đồng: bảo hiểm xe máy, bảo hiểm sức khỏe, bảo hiểm du lịch. Mỗi loại có bộ thông tin cực kỳ khác nhau — xe máy cần biển số, dung tích; du lịch cần điểm đến, ngày khởi hành, số người. Nếu model thành bảng phẳng, đó sẽ là cơn ác mộng cột rỗng.
Họ dùng policy_details JSON cho phần đặc thù từng loại, kết hợp một cột product_type thật để phân loại. Ban đầu mọi thứ chạy tốt. Nhưng đến khi cơ quan quản lý yêu cầu báo cáo "tổng phí bảo hiểm du lịch theo điểm đến", đội data khốn khổ vì phải bóc policy_details->'$.diem_den' trên hàng triệu hợp đồng, không có index.
Bài học rút ra: JSON đúng về mặt mô hình hóa, nhưng họ đã thiếu dự đoán nhu cầu báo cáo. Giải pháp về sau: với PostgreSQL, họ tạo generated column trích diem_den ra thành cột ảo có index — vừa giữ linh hoạt, vừa báo cáo nhanh. Đây là lời nhắc: hãy hỏi sớm "trường nào sẽ bị truy vấn?" ngay từ giai đoạn thiết kế.
Hướng dẫn từng bước
Khi đứng trước một thuộc tính và phân vân "cột thật hay JSON?", hãy đi qua quy trình sau:
Bước 1 — Liệt kê các thuộc tính và mức độ ổn định. Với mỗi thuộc tính, hỏi: nó xuất hiện ở mọi bản ghi hay chỉ một nhóm? Nếu chỉ một nhóm nhỏ, nó là ứng viên cho JSON.
Bước 2 — Hỏi câu hỏi vàng về truy vấn. "Người dùng hoặc báo cáo có cần lọc, sắp xếp, join, hay tổng hợp theo trường này không?" Nếu CÓ → cột thật. Nếu KHÔNG (chỉ hiển thị, đọc nguyên khối) → JSON là ứng viên hợp lệ.
Bước 3 — Kiểm tra ràng buộc nghiệp vụ. Trường này có cần NOT NULL, khóa ngoại, hay ràng buộc giá trị nghiêm ngặt không? Nếu có, ưu tiên cột thật, vì database không tự kiểm tra bên trong JSON.
Bước 4 — Đánh giá tính lồng nhau và tính bất biến. Dữ liệu có dạng cây (đơn hàng → dòng hàng → tùy chọn) và được lưu như một snapshot không đổi? Đó là điểm cộng lớn cho JSON.
Bước 5 — Thiết kế schema "lai" (hybrid). Phương án tốt nhất gần như luôn là kết hợp: cột thật cho phần xương sống và phần cần truy vấn; một cột JSON cho phần linh hoạt còn lại.
Bước 6 — Định nghĩa "JSON schema" trong data dictionary. Dù database không bắt buộc, bạn phải tài liệu hóa: cột JSON này chứa những khóa nào, kiểu gì, ý nghĩa ra sao, khóa nào bắt buộc. Đây là deliverable quan trọng nhất của một BA trong tình huống này.
Bước 7 — Lên kế hoạch tối ưu nếu cần. Nếu lường trước nhu cầu báo cáo, đề xuất generated/computed column hoặc index trên đường dẫn JSON cụ thể.
Lỗi thường gặp & mẹo
Lỗi 1 — Dùng JSON như cái cớ để khỏi suy nghĩ. Đây là lỗi nguy hiểm nhất. Một số đội nhét mọi thứ vào một cột data JSON để "khỏi phải làm migration". Sau một năm, không ai biết bên trong có gì, mỗi dev tự đặt tên khóa theo ý mình (mau_sac, color, colour cùng tồn tại). Mẹo: JSON là quyết định thiết kế có chủ đích, không phải đường tắt để né data modeling.
Lỗi 2 — Đặt dữ liệu cần báo cáo vào JSON. Khi sếp hỏi "doanh thu theo điểm đến", bạn sẽ hối hận. Mẹo: luôn hỏi trước "ba câu báo cáo phổ biến nhất sẽ là gì?" rồi đảm bảo những trường đó là cột thật hoặc có index.
Lỗi 3 — Không kiểm soát tên khóa. Vì database không ép buộc, tên khóa dễ loạn. Mẹo: lập một bảng quy ước đặt tên khóa JSON, thống nhất tiếng Việt không dấu hoặc tiếng Anh, và đưa vào data dictionary.
Lỗi 4 — Lồng quá sâu. JSON lồng 5–6 tầng rất khó đọc và truy vấn. Mẹo: giữ độ sâu tối đa 2–3 tầng; nếu sâu hơn, có lẽ đó nên là một bảng quan hệ riêng.
Lỗi 5 — Quên rằng quan hệ thật vẫn nên là bảng thật. Đừng nhồi danh sách order_items vào JSON nếu bạn cần thống kê sản phẩm bán chạy. Mẹo: nếu cần truy vấn ngược (từ chi tiết tìm về cha), hãy dùng bảng quan hệ chuẩn.
Mẹo bổ sung: Với MySQL/MariaDB, ưu tiên kiểu JSON (đã có validate cú pháp) thay vì lưu trong TEXT. Với PostgreSQL, gần như luôn chọn jsonb thay vì json để có index và hiệu năng tốt hơn.
Bài tập thực hành
Bài tập 1 — Phân loại thuộc tính. Cho một hệ thống đặt phòng khách sạn (kiểu Traveloka). Bạn có các thuộc tính: ten_khach, ngay_nhan_phong, ngay_tra_phong, tong_tien, tien_nghi_phong (wifi, máy lạnh, bồn tắm... mỗi khách sạn một khác), yeu_cau_dac_biet (ghi chú tự do của khách), loai_phong. Hãy xếp mỗi thuộc tính vào "cột thật" hay "JSON", và viết một câu lý do cho mỗi quyết định.
Bài tập 2 — Thiết kế cột JSON. Một phòng khám đa khoa muốn lưu kết quả xét nghiệm. Mỗi loại xét nghiệm (máu, nước tiểu, X-quang) có các chỉ số khác nhau. Hãy thiết kế một bảng lab_results với các cột thật phù hợp và một cột result_data JSON. Viết một ví dụ JSON cho xét nghiệm máu với ít nhất 3 chỉ số.
Bài tập 3 — Viết entry data dictionary. Lấy cột JSON bạn vừa thiết kế ở Bài tập 2, viết một entry data dictionary mô tả: tên cột, mục đích, danh sách các khóa kỳ vọng, kiểu dữ liệu mỗi khóa, khóa nào bắt buộc.
Bài tập 4 — Phản biện. Một đồng nghiệp đề xuất lưu toàn bộ thông tin khách hàng (tên, email, số điện thoại, lịch sử mua hàng) vào một cột customer_data JSON "cho gọn". Hãy viết 3 lý do thuyết phục để phản đối thiết kế này.
Tóm tắt
JSON và dữ liệu bán cấu trúc là một công cụ mạnh trong bộ đồ nghề data modeling của bạn — nhưng là công cụ cần dùng có chủ đích, không phải để né tránh việc thiết kế nghiêm túc. Hãy ghi nhớ các điểm cốt lõi:
- Cân nhắc cột JSON khi schema linh hoạt/đang tiến hóa, thuộc tính biến đổi theo từng bản ghi, dữ liệu lồng nhau, hoặc để tránh nhiều cột rỗng.
- Đánh đổi của JSON là mất ràng buộc tự động, truy vấn khó hơn và khó tài liệu hóa.
- Nguyên tắc vàng: trường cần lọc/join/báo cáo → cột thật; trường đọc-ghi nguyên khối và biến đổi → JSON.
- Thiết kế lai (hybrid) — cột xương sống thật + cột JSON linh hoạt — gần như luôn là lựa chọn tốt nhất.
- Với tư cách BA, deliverable quan trọng nhất của bạn là tài liệu hóa "schema ngầm" của cột JSON trong data dictionary, vì database sẽ không làm điều đó thay bạn.