Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 34 — Database Design Best Practices

Data Modeling and ERD Mastery Bài 34/60

Mở đầu — vì sao bài này quan trọng

Bạn đã đi qua hành trình dài: hiểu entity, attribute, khóa chính, quan hệ, chuẩn hóa, các mô hình conceptual–logical–physical. Bạn biết "vẽ" được một sơ đồ đúng về mặt lý thuyết. Nhưng có một khoảng cách rất lớn giữa một thiết kế đúng và một thiết kế tốt. Một thiết kế đúng thì chạy được; một thiết kế tốt thì người sau bạn — lập trình viên, DBA, BA kế nhiệm, hay chính bạn sau sáu tháng — vẫn đọc hiểu, mở rộng và bảo trì được mà không phải đoán mò.

Tôi đã từng tiếp quản một hệ thống quản lý đơn hàng của một chuỗi bán lẻ ở TP.HCM, nơi mỗi bảng được đặt tên theo một quy ước khác nhau: tblKhachHang, Orders, chi_tiet_dh, PRODUCT_MASTER. Chỉ riêng việc đoán xem khóa ngoại nào trỏ tới đâu đã ngốn của đội tôi gần hai tuần. Không có lỗi cú pháp nào ở đây cả — mọi thứ "đúng". Nhưng nó là một cơn ác mộng để làm việc cùng.

Bài học này không dạy bạn thêm một khái niệm mới về mô hình hóa. Nó dạy bạn những thói quen kỷ luật biến một schema đúng thành một schema mà cả đội tự hào khi làm việc cùng: quy ước đặt tên, tính nhất quán, khả năng đọc hiểu, và những nguyên tắc thiết kế đã được kiểm chứng qua hàng nghìn dự án. Đây là phần "tay nghề" mà không sách giáo khoa nào nhấn mạnh đủ, nhưng lại quyết định bạn được đánh giá là một người làm mô hình dữ liệu nghiệp dư hay chuyên nghiệp.

Khái niệm cốt lõi

Quy ước đặt tên (Naming conventions)

Đây là thứ đầu tiên người khác nhìn thấy khi mở schema của bạn, và là thứ phản ánh rõ nhất bạn có kỷ luật hay không.

Bảng (tables):

  • Dùng chữ thường, kiểu snake_case: customer, order_item, payment_transaction. Tránh CamelCase hay khoảng trắng vì nhiều DBMS phân biệt hoa thường tùy cấu hình, và snake_case đọc nhất quán trên mọi nền tảng.
  • Chọn số ít hay số nhiều và giữ nguyên suốt dự án. customer hay customers đều chấp nhận được — điều tối kỵ là trộn cả hai. Trường phái số ít coi bảng là "mẫu một bản ghi" (customer = một khách hàng); trường phái số nhiều coi bảng là "tập hợp". Hãy chọn một và ghi vào tài liệu chuẩn của đội.
  • Bảng nối (junction table) cho quan hệ nhiều-nhiều nên ghép tên hai bảng: student_course, order_product.
Cột (columns):
  • Cũng snake_case, mô tả rõ nghĩa: first_name, created_at, total_amount.
  • Khóa chính nên nhất quán: hoặc luôn là id, hoặc luôn là <tên_bảng>_id (ví dụ customer_id). Cách thứ hai giúp khóa ngoại và khóa chính cùng tên, giảm nhầm lẫn khi JOIN.
  • Khóa ngoại nên mang tên bảng được tham chiếu: cột trỏ tới customer nên là customer_id, không phải cust hay ma_kh.
  • Cột boolean nên có tiền tố diễn đạt trạng thái đúng/sai: is_active, has_paid, is_deleted.
  • Cột thời gian dùng hậu tố _at cho timestamp (created_at, updated_at) và _date cho ngày (birth_date, due_date).
Tránh từ khóa dành riêng (reserved words): đừng đặt bảng/cột tên order, user, group, date, value nếu có thể, vì chúng trùng từ khóa SQL và buộc bạn phải bọc backtick/ngoặc kép khắp nơi. order đặc biệt nguy hiểm — nhiều đội đổi thành sales_order hoặc purchase_order để vừa rõ nghĩa vừa tránh xung đột.

Tính nhất quán hơn cả sự hoàn hảo

Một nguyên tắc tôi muốn bạn khắc cốt ghi tâm: một quy ước nhất quán nhưng không hoàn hảo còn tốt hơn nhiều quy ước "đúng" nhưng lẫn lộn. Bộ não con người dựa vào pattern. Khi mọi khóa chính đều là id, mọi timestamp đều là created_at/updated_at, người đọc có thể đoán được cấu trúc cột mà không cần tra cứu. Sự nhất quán biến schema của bạn thành tài liệu tự giải thích.

Các nguyên tắc thiết kế nền tảng

  • Mỗi bảng đại diện cho đúng một khái niệm. Nếu bảng customer của bạn có cả invoice_numbershipping_carrier, đó là dấu hiệu bạn đang nhồi nhiều khái niệm vào một chỗ.
  • Luôn có khóa chính. Mọi bảng cần một định danh duy nhất, ổn định, không bao giờ thay đổi. Surrogate key (id tự tăng hoặc UUID) thường an toàn hơn natural key cho mục đích này.
  • Ưu tiên ràng buộc ở tầng database, không phó mặc cho ứng dụng. NOT NULL, FOREIGN KEY, UNIQUE, CHECK là tuyến phòng thủ cuối cùng cho tính toàn vẹn dữ liệu. Ứng dụng có thể có bug; database thì luôn thực thi ràng buộc.
  • Mặc định bao gồm cột audit. created_at, updated_at, và tùy hệ thống là created_by, updated_by. Chúng cực kỳ rẻ khi thêm ngay từ đầu và cực kỳ đắt khi phải bổ sung về sau cho dữ liệu cũ.
  • Soft delete thay vì xóa cứng với dữ liệu nghiệp vụ quan trọng: dùng is_deleted hoặc deleted_at để giữ lịch sử và khả năng khôi phục.
  • Một kiểu dữ liệu cho một loại thông tin. Tiền tệ luôn DECIMAL, không bao giờ FLOAT. Mã trạng thái nên có tập giá trị xác định, không phải text tự do.

Khả năng đọc hiểu và khả năng mở rộng

Thiết kế tốt là thiết kế dự liệu được tương lai mà không vẽ vời thừa thãi. Đừng tạo sẵn 20 cột "phòng khi cần". Nhưng hãy thiết kế sao cho việc thêm một loại sản phẩm mới, một phương thức thanh toán mới không buộc bạn phải sửa cấu trúc bảng. Đây là nghệ thuật cân bằng: đủ linh hoạt để thích ứng, đủ chặt chẽ để bảo vệ dữ liệu.

Tình huống thực tế

Tình huống 1 — Tiki và bài học về quy ước khóa ngoại

Giả định một đội kỹ thuật tại một sàn thương mại điện tử lớn của Việt Nam đang gộp ba module được ba nhóm khác nhau xây dựng: giỏ hàng, thanh toán, và kho vận. Module giỏ hàng dùng khóa ngoại userId, module thanh toán dùng customer_ref, module kho vận dùng kh_id. Cả ba đều trỏ về cùng một bảng người dùng.

Khi viết một báo cáo doanh thu theo khách hàng phải JOIN cả ba module, đội phân tích mất rất nhiều thời gian chỉ để xác minh "ba cột này có thật sự cùng tham chiếu một thứ không". Tệ hơn, một lập trình viên đã JOIN nhầm kh_id (thực ra là mã kho — kho hàng) với bảng người dùng vì tên cột mơ hồ, tạo ra một báo cáo doanh thu sai lệch suốt một quý.

Bài học: nếu ngay từ đầu có một quy ước bắt buộc — mọi khóa ngoại trỏ về bảng user đều phải tên user_id — thì lỗi này gần như không thể xảy ra. Một dòng quy tắc trong tài liệu chuẩn của đội rẻ hơn nhiều so với một báo cáo tài chính sai.

Tình huống 2 — Ngân hàng số và cột audit cứu một cuộc điều tra

Một ngân hàng số ở Đông Nam Á (kiểu như Timo hay Cake) thiết kế bảng account_transaction ban đầu chỉ có các cột nghiệp vụ thuần: transaction_id, account_id, amount, transaction_type. Để cho "gọn", đội bỏ qua các cột audit.

Sáu tháng sau, một khách hàng khiếu nại có giao dịch lạ. Đội điều tra cần biết: bản ghi này được tạo lúc nào, bởi tiến trình nào, có bị sửa không. Nhưng không có created_at, không có created_by, không có updated_at. Họ phải lục log hệ thống thô — một quá trình tốn hàng chục giờ và vẫn không chắc chắn. Sau sự cố, đội buộc phải migration thêm cột audit cho hàng chục triệu bản ghi cũ, với giá trị created_at chỉ có thể ước lượng gần đúng.

Bài học: cột audit gần như miễn phí khi thêm lúc thiết kế, nhưng vô giá khi cần truy vết. Với dữ liệu tài chính, audit và khả năng truy vết không phải tùy chọn — chúng là yêu cầu sống còn, và nên là mặc định trong mọi bảng.

Tình huống 3 — Startup logistics và cái bẫy của "tên cho nhanh"

Một startup giao hàng ở Hà Nội cần ra mắt MVP gấp. Lập trình viên đặt tên bảng và cột theo kiểu "gõ cho nhanh": bảng dh (đơn hàng), cột tt (trạng thái), sl (số lượng), gc (ghi chú). Hệ thống chạy tốt, gọi vốn thành công, đội mở rộng từ 2 lên 12 người.

Lúc này thảm họa bắt đầu. Người mới hỏi tt nghĩa là gì — trạng thái? thanh toán? thời gian? Một người khác đoán gc là "giá cuối" thay vì "ghi chú" và viết logic tính tiền sai. Mỗi cột viết tắt trở thành một câu đố. Cuối cùng đội phải dành nguyên một sprint để đổi tên toàn bộ schema và sửa code phụ thuộc — một chi phí lớn hơn nhiều lần so với việc gõ đầy đủ order, status, quantity, note ngay từ đầu.

Bài học: tên rõ nghĩa không phải xa xỉ phẩm. Bạn gõ tên cột một lần nhưng đọc nó hàng nghìn lần. Tốc độ gõ tiết kiệm được không bao giờ bù nổi thời gian giải mã về sau.

Hướng dẫn từng bước

Khi bạn bắt đầu một thiết kế mới hoặc rà soát một thiết kế hiện có, hãy đi theo trình tự sau:

  • Thống nhất một bộ quy ước trước khi vẽ bảng đầu tiên. Viết ra: số ít hay số nhiều, id hay <table>_id, hậu tố thời gian, tiền tố boolean. Một trang A4 là đủ. Mọi người trong đội ký vào đó (theo nghĩa đồng thuận).
  • Đặt tên bảng theo khái niệm nghiệp vụ, không theo viết tắt. Hỏi: "một người mới vào hôm nay đọc tên này có hiểu ngay không?" Nếu không, đổi tên.
  • Với mỗi bảng, định nghĩa khóa chính trước tiên. Mặc định dùng surrogate key (id) trừ khi có lý do mạnh để dùng natural key.
  • Thêm cột audit như một template mặc định. Mọi bảng nghiệp vụ tự động có created_at, updated_at. Cân nhắc created_by, updated_by, deleted_at tùy độ nhạy cảm dữ liệu.
  • Đặt tên khóa ngoại theo bảng được tham chiếu và khai báo ràng buộc FOREIGN KEY ngay. Đừng để quan hệ chỉ tồn tại "trong đầu".
  • Khai báo ràng buộc toàn vẹn ở tầng database. Với mỗi cột, hỏi: nó có được null không (NOT NULL)? Có cần duy nhất không (UNIQUE)? Có giới hạn giá trị không (CHECK)?
  • Chọn kiểu dữ liệu phù hợp ngữ nghĩa. Tiền dùng DECIMAL, không FLOAT. Trạng thái dùng tập giá trị xác định. Văn bản dài dùng TEXT, ngắn dùng VARCHAR có giới hạn hợp lý.
  • Rà soát chéo. Cho một đồng nghiệp đọc schema mà không giải thích. Chỗ nào họ phải hỏi là chỗ tên hoặc cấu trúc chưa đủ rõ.

Lỗi thường gặp & mẹo

Trộn nhiều quy ước trong cùng dự án. Đây là lỗi phổ biến nhất khi nhiều người cùng làm. Mẹo: dùng linter schema hoặc một checklist review để bắt lỗi tự động trước khi merge.

Đặt tên theo công nghệ thay vì nghiệp vụ. Cột varchar_field_1 hay bảng temp_data nói lên rằng bạn chưa hiểu dữ liệu mình đang mô hình hóa. Tên phải phản ánh ý nghĩa nghiệp vụ.

Lạm dụng viết tắt và tiếng Việt không dấu lẫn lộn. ma_kh, tongtien, ngaytao trộn với customer_id, total tạo ra schema chắp vá. Mẹo: chọn một ngôn ngữ (thường là tiếng Anh cho tên kỹ thuật) và một phong cách, giữ nhất quán.

Dùng FLOAT cho tiền. FLOAT gây sai số làm tròn — 0.1 + 0.2 không bằng 0.3. Với tiền tệ, dùng DECIMAL/NUMERIC. (Bài về Money & Currency sẽ đào sâu, nhưng đây là nguyên tắc best practice cốt lõi cần nhớ.)

Bỏ qua ràng buộc vì "ứng dụng sẽ lo". Ứng dụng có bug, có nhiều phiên bản, có người gọi trực tiếp vào DB. Ràng buộc database là phòng tuyến cuối không bao giờ ngủ.

Xóa cứng dữ liệu nghiệp vụ. Một lệnh DELETE nhầm có thể xóa lịch sử không khôi phục được. Mẹo: dùng soft delete cho dữ liệu quan trọng, và chỉ cho phép xóa cứng qua quy trình kiểm soát.

Mẹo vàng: tạo một "table template" chuẩn của đội — một đoạn DDL mẫu đã có sẵn id, created_at, updated_at, quy ước đặt tên. Mỗi bảng mới bắt đầu từ template này. Đây là cách rẻ nhất để ép sự nhất quán mà không cần ai phải nhớ.

Bài tập thực hành

Bài 1 — Sửa schema lộn xộn. Cho các bảng và cột sau, hãy viết lại theo một bộ quy ước nhất quán và giải thích từng thay đổi:

  • Bảng tblKhachHang với cột KH_ID, Ten, dienthoai, NgayTao
  • Bảng Orders với cột id, cust, tongtien (đang dùng FLOAT), tt
  • Bảng chitiet với cột order_ref, sp_id, sl
Bài 2 — Thiết kế template. Viết một đoạn DDL "table template" của riêng bạn, bao gồm: quy ước khóa chính, cột audit mặc định, và ít nhất hai ràng buộc toàn vẹn. Ghi chú lý do cho từng lựa chọn.

Bài 3 — Phản biện. Một đồng nghiệp lập luận: "Đặt cột audit vào mọi bảng là thừa, làm phình database." Hãy viết một đoạn phản hồi 4–5 câu, nêu khi nào lập luận đó có lý và khi nào nó sai.

Tóm tắt

Best practices trong thiết kế database không phải là những quy tắc cứng nhắc để học thuộc, mà là kỷ luật nghề nghiệp giúp thiết kế của bạn vượt qua thử thách lớn nhất: thời gian và con người kế nhiệm. Ba trụ cột bạn cần mang theo: quy ước đặt tên nhất quán (snake_case, tên rõ nghĩa, khóa ngoại theo bảng tham chiếu, một quy ước số ít/số nhiều), sự nhất quán quan trọng hơn sự hoàn hảo (vì não người dựa vào pattern), và bảo vệ tính toàn vẹn ở tầng database (ràng buộc, cột audit, soft delete, kiểu dữ liệu đúng ngữ nghĩa).

Như ba tình huống đã cho thấy: một quy ước khóa ngoại thiếu nhất quán có thể tạo ra báo cáo tài chính sai; cột audit bị bỏ qua biến một cuộc điều tra thành ác mộng; và những cái tên "cho nhanh" ngốn cả một sprint để sửa. Tin tốt là tất cả những thảm họa này đều phòng được gần như miễn phí — chỉ cần kỷ luật ngay từ dòng DDL đầu tiên. Hãy biến những thói quen này thành phản xạ, và schema của bạn sẽ là thứ người sau cảm ơn bạn, chứ không phải nguyền rủa.