Một thiết kế cơ sở dữ liệu tồi thường không lộ ra ngay ngày đầu. Ứng dụng vẫn chạy, dữ liệu vẫn lưu được. Nhưng vài tháng sau, bạn phát hiện cùng một khách hàng có ba địa chỉ khác nhau ở ba nơi, sửa tên sản phẩm phải cập nhật hàng nghìn dòng, và báo cáo doanh thu cho ra hai con số không khớp. Chuẩn hoá (normalization) là bộ nguyên tắc giúp tránh những rắc rối đó. Tuy nhiên, chuẩn hoá tuyệt đối không phải lúc nào cũng tốt; đôi khi bạn cần phi chuẩn hoá có chủ đích. Bài viết này giúp bạn hiểu cả hai mặt.

Vì sao cần chuẩn hoá?

Mục tiêu cốt lõi của chuẩn hoá là mỗi sự thật chỉ được lưu ở một nơi. Khi dữ liệu bị lặp lại, bạn sẽ gặp các bất thường (anomaly):

  • Bất thường khi cập nhật: đổi số điện thoại khách hàng nhưng quên một vài dòng, dữ liệu trở nên mâu thuẫn.
  • Bất thường khi thêm: không thể lưu thông tin một sản phẩm mới nếu chưa có đơn hàng nào chứa nó.
  • Bất thường khi xoá: xoá đơn hàng cuối cùng của khách hàng làm mất luôn thông tin khách hàng.

Ví dụ: một bảng "làm tất cả"

Giả sử bạn lưu đơn hàng trong một bảng duy nhất với các cột: mã đơn, ngày đặt, tên khách, email khách, danh sách sản phẩm (dạng chuỗi "Áo, Quần, Mũ"), giá từng món. Thiết kế này nhanh gọn ban đầu nhưng mắc gần như mọi lỗi kể trên. Hãy chuẩn hoá nó từng bước.

Dạng chuẩn 1 (1NF): giá trị nguyên tử

Mỗi ô chỉ chứa một giá trị duy nhất, không có danh sách hay nhóm lặp lại. Cột "danh sách sản phẩm" vi phạm 1NF. Giải pháp là tách thành bảng chi tiết đơn hàng, mỗi dòng ứng với một sản phẩm trong một đơn.

Dạng chuẩn 2 (2NF): phụ thuộc vào toàn bộ khoá

Áp dụng cho bảng có khoá chính gồm nhiều cột. Mọi cột không phải khoá phải phụ thuộc vào toàn bộ khoá, không chỉ một phần.

Trong bảng chi tiết đơn hàng với khoá (order_id, product_id), cột quantity phụ thuộc vào cả hai, hợp lệ. Nhưng cột product_name chỉ phụ thuộc vào product_id, vi phạm 2NF. Hãy chuyển nó sang bảng products.

Dạng chuẩn 3 (3NF): không phụ thuộc bắc cầu

Các cột không phải khoá không được phụ thuộc vào một cột không phải khoá khác. Trong bảng orders, nếu có customer_id và cả customer_email, thì email phụ thuộc vào khách hàng chứ không phải vào đơn hàng. Hãy tách ra bảng customers.

Kết quả sau khi chuẩn hoá:

CREATE TABLE customers (id BIGINT PRIMARY KEY, name TEXT, email TEXT UNIQUE);
CREATE TABLE products  (id BIGINT PRIMARY KEY, name TEXT, price NUMERIC(12,2));
CREATE TABLE orders (
  id BIGINT PRIMARY KEY,
  customer_id BIGINT REFERENCES customers(id),
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE order_items (
  order_id BIGINT REFERENCES orders(id),
  product_id BIGINT REFERENCES products(id),
  quantity INT NOT NULL,
  unit_price NUMERIC(12,2) NOT NULL,
  PRIMARY KEY (order_id, product_id)
);

Có những dạng chuẩn cao hơn như BCNF, 4NF, 5NF, nhưng với phần lớn ứng dụng nghiệp vụ, đạt tới 3NF đã là nền tảng vững chắc.

Bạn có để ý cột unit_price trong order_items không? Thoạt nhìn nó trùng với products.price, nhưng thực ra đây là hai sự thật khác nhau: giá hiện tại của sản phẩm và giá tại thời điểm khách mua. Hiểu đúng ý nghĩa nghiệp vụ quan trọng hơn áp dụng máy móc các quy tắc.

Khi nào nên phi chuẩn hoá?

Chuẩn hoá giúp dữ liệu nhất quán, nhưng cái giá là phải JOIN nhiều bảng khi đọc. Với hệ thống đọc nhiều, báo cáo phức tạp hoặc dữ liệu cực lớn, các phép join và tổng hợp lặp đi lặp lại có thể trở thành gánh nặng. Lúc đó, phi chuẩn hoá, tức chủ động lưu dư thừa dữ liệu để đọc nhanh hơn, là lựa chọn hợp lý.

Các tình huống thường gặp:

  1. Lưu giá trị tổng hợp: thêm cột order_count vào bảng customers thay vì đếm mỗi lần hiển thị.
  2. Sao chép cột hay dùng: lưu customer_name trong bảng đơn hàng để trang danh sách đơn không phải join.
  3. Bảng phục vụ báo cáo: tạo bảng hoặc kho dữ liệu riêng, tổ chức theo mô hình sao (star schema) cho phân tích.
  4. Lưu ảnh chụp dữ liệu lịch sử: như unit_price ở trên, hoặc địa chỉ giao hàng tại thời điểm đặt.

Phi chuẩn hoá có kiểm soát

Phi chuẩn hoá luôn đi kèm rủi ro dữ liệu lệch nhau. Một số cách giảm rủi ro:

  • Materialized view: PostgreSQL cho phép lưu kết quả truy vấn thành một view vật lý và làm mới bằng REFRESH MATERIALIZED VIEW. Dữ liệu gốc vẫn chuẩn hoá, phần dư thừa được quản lý tập trung.
  • Trigger hoặc logic ứng dụng nhất quán: cập nhật cột dư thừa trong cùng transaction với dữ liệu gốc.
  • Cột jsonb có chọn lọc: lưu dữ liệu ít truy vấn theo cấu trúc, nhưng tránh biến cả bảng thành "túi đựng JSON".
  • Ghi rõ lý do trong tài liệu thiết kế để người sau biết cột nào là bản sao và nguồn gốc của nó ở đâu.

Nguyên tắc thực tế

  • Bắt đầu bằng thiết kế chuẩn hoá (khoảng 3NF). Đây là mặc định an toàn.
  • Chỉ phi chuẩn hoá khi có bằng chứng: truy vấn chậm đo được, không giải quyết được bằng index hay viết lại truy vấn.
  • Ưu tiên giải pháp ít xâm lấn trước: index, cache, materialized view, rồi mới tới thay đổi schema.
  • Tách biệt OLTP và OLAP: hệ thống giao dịch giữ chuẩn hoá, hệ thống phân tích có thể phi chuẩn hoá mạnh.

Kết luận

Chuẩn hoá và phi chuẩn hoá không đối lập mà bổ sung cho nhau. Chuẩn hoá đem lại dữ liệu đúng, nhất quán và dễ bảo trì; phi chuẩn hoá đem lại tốc độ đọc khi hệ thống thật sự cần. Người thiết kế giỏi là người hiểu rõ nghiệp vụ, bắt đầu từ một mô hình chuẩn hoá sạch sẽ, và chỉ đánh đổi khi có lý do rõ ràng cùng cơ chế giữ dữ liệu nhất quán.

Nguồn tham khảo