Skip to content

Chuẩn hoá & phá chuẩn ​

Chia dữ liệu thành bảng và quan hệ sao cho mỗi sự thật chỉ lưu một chỗ. Phá chuẩn khi có lý do đo được, và luôn kèm cách giữ các bản sao đồng bộ.

Bắt đầu từ cách dữ liệu được dùng ​

  1. Thực thể và quan hệ: khách hàng, đơn hàng, sản phẩm; một khách có nhiều đơn, một đơn có nhiều sản phẩm.
  2. Bất biến nghiệp vụ: email không trùng, tồn kho không âm, đơn đã giao không quay về “chờ xác nhận”. Những điều này nên được database giữ bằng ràng buộc, không chỉ dựa vào code.
  3. Mẫu truy cập: màn hình nào đọc gì, ghi gì, bao nhiêu lần mỗi giây. Schema và index phục vụ các query thật, không phải sơ đồ đẹp.
  4. Vòng đời dữ liệu: giữ bao lâu, có phải lưu lịch sử thay đổi không (Versioning & lịch sử), có cần xoá theo yêu cầu người dùng không (Soft delete).

Chuẩn hoá: mỗi sự thật lưu một chỗ ​

Khi cùng một thông tin nằm ở nhiều dòng, sớm muộn các bản sao sẽ lệch nhau. Chuẩn hoá chia dữ liệu để tránh ba loại bất thường: sửa (phải sửa nhiều chỗ, sót một chỗ là sai), xoá (xoá một thứ làm mất luôn thông tin khác), thêm (không thêm được thứ này nếu chưa có thứ kia).

  • 1NF: mỗi ô một giá trị nguyên tử. Không lưu '1,5,9' trong một cột, không đặt phone1, phone2, phone3: tách thành bảng con.
  • 2NF: với khoá ghép, mọi cột phải phụ thuộc vào toàn bộ khoá. Trong order_items(order_id, product_id, …), cột product_name chỉ phụ thuộc product_id, nên thuộc về bảng products.
  • 3NF: cột không phụ thuộc bắc cầu qua cột khác. orders.customer_phone phụ thuộc vào customer_id chứ không phải vào đơn hàng, nên thuộc về customers.
  • BCNF: mọi phụ thuộc đều phải bắt nguồn từ một khoá ứng viên. Hiếm gặp khác biệt với 3NF trong thực tế; biết tên là đủ.

Thử với một bảng “phẳng” gộp mọi thứ vào một chỗ và bản đã chuẩn hoá:

Thao tác
4 dòng phải sửabảng phẳng
1 dòngđã chuẩn hoá

Bảng phẳng lặp số điện thoại của An ở mọi đơn: phải sửa 4 dòng, sót một dòng là An có hai số khác nhau (bất thường khi sửa). Bản chuẩn hoá chỉ sửa một dòng trong customers.

Bảng phẳng: orders_flat
idkháchSĐTsản phẩmgiáSL
1An0999Áo thun150.0002
2An0999Quần jean400.0001
3Bình0902Áo thun150.0001
4An0999Giày900.0001
5Chi0903Quần jean400.0002
6Bình0902Giày900.0001
7Chi0903Áo thun150.0003
8Dũng0904Mũ120.0001
9An0999Áo thun150.0001
Đã chuẩn hoá
customers
idtênSĐT
1An0999
2Bình0902
3Chi0903
4Dũng0904
products
idtêngiá
1Áo thun150.000
2Quần jean400.000
3Giày900.000
4Mũ120.000
orders (rút gọn: mỗi đơn một sản phẩm)
idcustomer_idproduct_idunit_priceSL
111150.0002
212400.0001
321150.0001
413900.0001
532400.0002
623900.0001
731150.0003
844120.0001
911150.0001

Phá chuẩn có chủ đích ​

Chuẩn hoá trước, phá chuẩn khi đo được rằng đường đọc nóng đang trả giá cho JOIN hoặc phép đếm. Mỗi bản sao là một thứ phải giữ đồng bộ.

Kỹ thuậtVí dụGiữ đồng bộ bằng
Cột đếmposts.comment_count thay cho COUNT(*) mỗi lần hiển thịCập nhật trong cùng transaction với lệnh ghi comment, hoặc qua CDC
Bảng tổng hợpDoanh thu theo ngày tính sẵnJob định kỳ, hoặc cập nhật tăng dần qua CDC
Chép cột để hiển thịproducts.seller_name để trang danh sách không phải joinCập nhật khi tên đổi + job đối soát định kỳ
Read model riêngBảng hoặc index search dựng sẵn cho một màn hìnhCDC / event (CQRS)

Snapshot không phải phá chuẩn ​

order_items.unit_price trông giống bản sao của products.price, nhưng thực chất là một sự thật khác: giá tại thời điểm đặt hàng. Đổi giá sản phẩm không được làm đổi giá trong các đơn cũ. Tương tự với địa chỉ giao hàng, tên sản phẩm trên hoá đơn, tỷ giá áp dụng.

Câu hỏi để phân biệt: khi bản gốc đổi, giá trị này có phải đổi theo không? Có thì đó là bản sao (phá chuẩn, phải đồng bộ). Không thì đó là snapshot (dữ liệu lịch sử, không bao giờ cập nhật).

Ghi nhớ

  1. Chuẩn hoá tới 3NF trước; phá chuẩn khi đo được, kèm cơ chế đồng bộ.
  2. Ba bất thường: sửa, xoá, thêm.
  3. Giá, địa chỉ trong đơn là snapshot, không phải dữ liệu thừa.