Cách Mình Thiết Kế Dữ Liệu

Nguyên tắc thiết kế database schema, TypeScript types, và cấu trúc dữ liệu

Thiết kế data tốt thì query dễ, update đúng, và thêm tính năng sau không phải refactor lại hết. Câu hỏi quan trọng nhất khi bắt đầu thiết kế schema:

"Mình sẽ cần trả lời những câu hỏi gì từ dữ liệu này?"

Schema nên làm cho query phổ biến thì dễ viết, còn query hiếm thì vẫn khả thi.

Nguyên Tắc

1. Model Theo Domain, Không Phải Theo UI

Database phản ánh khái niệm business, không phải layout màn hình.

Đặt tên bảng theo UI kiểu DashboardData thì 6 tháng sau có dashboard thứ hai là loạn luôn. Model những thứ thật đi -- Users, Orders, Products, Sessions. UI chỉ là một cách nhìn vào dữ liệu thôi, không phải nguồn sự thật cho cấu trúc.

Cách kiểm tra nhanh: giải thích mỗi bảng cho một người không kỹ thuật mà chỉ dùng thuật ngữ business được không? Nếu phải chỉ vào màn hình mới giải thích được thì nên xem lại.

2. Lưu Lịch Sử Khi Cần Thiết

Câu hỏi khó nhất trong data modeling:

"Nên cập nhật record này, hay tạo record mới?"

Cập nhật tại chỗ khi chỉ trạng thái hiện tại là quan trọng, lịch sử không có giá trị business, và storage quan trọng hơn audit trail.

Tạo record mới (snapshot) khi cần trả lời "giá trị vào ngày X là gì?", compliance yêu cầu lịch sử, user cần so sánh thay đổi theo thời gian, hoặc undo/rollback là tính năng.

Mình hay dùng pattern session: nhóm dữ liệu liên quan vào session có timestamp. Mỗi lần check/run/import tạo session mới. Có lịch sử tự động mà không cần versioning phức tạp.

3. Normalize Trước, Denormalize Sau

Bắt đầu normalized: mỗi fact chỉ nằm ở một chỗ, không trùng lặp. Lý do đơn giản -- sửa một row là xong, không phải sửa hai mươi row.

Khi nào denormalize? Chỉ khi đo được vấn đề performance thật sự. Denormalize sớm quá thì data dễ bị lệch mà chẳng có lợi gì vì ban đầu đâu có vấn đề performance.

Mấy kiểu denormalize phổ biến:

  • Lưu count thay vì đếm mỗi lần
  • Copy tên user vào order (để tồn tại dù user bị xóa)
  • Pre-compute aggregate cho dashboard

4. Thiết Kế Cho Query

Trước khi tạo bảng, liệt kê ra giấy:

  1. 5 query phổ biến nhất sẽ chạy
  2. Các filter cần thiết (WHERE)
  3. Các sort cần thiết (ORDER BY)
  4. Các join cần thiết

Rồi thiết kế schema sao cho mấy query đó viết đơn giản. Schema mà khiến query phổ biến cần 5 join thì sai rồi.

Về index: mỗi column nằm trong WHERE, JOIN, hoặc ORDER BY chạy thường xuyên thì nên index. Nhưng đừng index mọi thứ -- index làm chậm write.

Khung Ra Quyết Định

"Nên tạo bảng mới hay thêm column?"

Tạo bảng mới khi dữ liệu có ID riêng, có thể nhiều record per parent (one-to-many), được tham chiếu bởi bảng khác, hoặc có lifecycle riêng (tạo/xóa độc lập).

Thêm column khi nó là thuộc tính của thứ đang có, chỉ có đúng một per row, và luôn tồn tại hoặc thay đổi cùng parent.

"Nên lưu hay tính toán?"

Lưu khi tính toán tốn kém và chạy thường xuyên, input hiếm khi thay đổi, hoặc cần giữ giá trị tại thời điểm đó (chứ không phải giá trị hiện tại).

Tính toán khi giá trị thay đổi liên tục, khó giữ consistency nếu lưu, hoặc tính toán rẻ.

Kết hợp cũng được: tính toán rồi cache kết quả, invalidate khi có thay đổi.

"Xử lý xóa dữ liệu thế nào?"

Chiến lượcKhi nào dùng
Hard delete (xóa thật)Data không có giá trị lâu dài, privacy yêu cầu, hoặc đơn giản quan trọng hơn khôi phục
Soft delete (deleted_at)User có thể muốn khôi phục, audit trail bắt buộc, hoặc reference từ bảng khác sẽ hỏng
Cascade delete (xóa luôn children)Children vô nghĩa nếu thiếu parent, muốn cleanup sạch sẽ
Restrict delete (chặn xóa)Children mồ côi gây lỗi data integrity, user nên dọn trước

"Field này nên dùng kiểu gì?"

Text vs. Enum: Có tập giá trị cố định thì enum (hoặc check constraint). Giá trị do user nhập hoặc hay thay đổi thì text.

Integer vs. UUID cho ID: Integer nhỏ gọn, nhanh, tuần tự. UUID thì generate ở đâu cũng được, không lộ thông tin, không cần phối hợp trung tâm.

Timestamp: Luôn lưu UTC. Dùng đúng kiểu timestamp/datetime, đừng lưu string. Có ý nghĩa business thì kèm timezone.

JSON column: Dùng cho data thực sự dynamic/schemaless. Tránh cho data cần query thường xuyên. Tốt để lưu raw API response hoặc metadata linh hoạt.

Sai Lầm Thường Gặp

Bảng "thùng rác"

Nhét mọi thứ vào một bảng, dùng nullable column cho các "loại" khác nhau. Dấu hiệu rõ nhất: nửa số column là NULL cho bất kỳ row nào, và có column type quyết định column nào khác hợp lệ.

Sửa: tách thành bảng riêng. Dùng inheritance nếu database hỗ trợ, hoặc đơn giản là bảng riêng với shared ID.

Thiếu index

Query đơn giản mất vài giây, database scan toàn bộ bảng.

Thêm index cho column trong WHERE, JOIN, và ORDER BY. Dùng EXPLAIN để confirm query dùng index.

Tối ưu sớm quá

App mới có 100 user mà đã thêm caching, denormalization, materialized view. Rồi logic sync phức tạp, data không nhất quán do cache invalidation fail.

Bắt đầu đơn giản đi. Đo lường. Chỉ tối ưu cái thực sự chậm thôi.

Lưu dữ liệu kiểu "String hóa"

Column kiểu metadata: "key1=value1;key2=value2" thì logic parse sẽ nằm rải rác khắp codebase. Dùng JSON column hoặc tách field riêng, để database lo type validation.

Thiếu constraint

Dựa vào application code để enforce rule dữ liệu thì trước sau gì cũng có bug cho data xấu lọt qua. Thêm constraint đi: NOT NULL, UNIQUE, FOREIGN KEY, CHECK. Database là tuyến phòng thủ cuối cùng.

Cách Đánh Giá Schema

Đang ổn nếu:

  • Query phổ biến đơn giản (tối đa 1-2 join)
  • Giải thích được mỗi bảng bằng thuật ngữ business
  • Constraint ngăn data không hợp lệ
  • Update sửa một row, không phải nhiều row
  • Trả lời được câu hỏi lịch sử khi cần

Cần xem lại nếu:

  • Query cần 5+ join
  • Cùng một fact lưu ở nhiều nơi
  • Column "type" quyết định column nào khác hợp lệ
  • App code phải work around giới hạn schema
  • Đã mất data mà sau này cần

Phát Triển Schema Theo Thời Gian

Thêm thì an toàn: nullable column mới hoặc bảng mới hiếm khi gây lỗi.

Xóa/đổi tên thì nguy hiểm: phải làm theo giai đoạn -- ngừng ghi vào column cũ, deploy code không đọc column cũ, rồi mới xóa.

Migration nên reversible: viết cả "up" và "down". Test migration down nữa, đừng bỏ qua.

Backfill cẩn thận: điền data vào column mới từ column cũ có thể chậm, chạy theo batch cho chắc.