Menu
ESC

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

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

Đang tải...

Bài 15 — Denormalization — When and Why

Data Modeling and ERD Mastery Bài 15/60

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

Suốt mấy bài vừa rồi, mình đã cùng nhau "mài giũa" mô hình dữ liệu cho thật chuẩn: tách bảng, loại bỏ trùng lặp, đưa về 1NF, 2NF, 3NF. Cảm giác như càng chuẩn hóa thì càng "sạch", càng đẹp. Và đúng là vậy thật — về mặt lý thuyết.

Nhưng rồi sẽ đến một ngày, sếp hoặc khách hàng gõ cửa và nói: "Cái màn hình báo cáo doanh thu của em sao load mất 8 giây? Khách hàng đang phàn nàn kìa." Bạn mở câu truy vấn ra xem, và nó đang JOIN qua bảy cái bảng vì mô hình của bạn quá chuẩn hóa. Lúc đó bạn mới nhận ra: chuẩn hóa tối đa không phải lúc nào cũng là chuẩn hóa tối ưu.

Đó chính là lúc denormalization (phi chuẩn hóa) bước vào sân khấu. Đây là một trong những kỹ năng phân biệt giữa một người mới học mô hình hóa dữ liệu và một Business Analyst hoặc Data Modeler thực thụ. Bởi vì biết cách chuẩn hóa là kiến thức cơ bản, nhưng biết khi nào nên cố tình phá vỡ chuẩn hóa — và phá như thế nào cho an toàn — mới là bản lĩnh.

Bài này không dạy bạn "làm trái" những gì đã học. Nó dạy bạn hiểu rằng mọi quyết định thiết kế đều là sự đánh đổi (trade-off), và denormalization là một công cụ có chủ đích để đổi lấy hiệu năng đọc dữ liệu, đổi lại việc bạn phải chấp nhận một số rủi ro nhất định.

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

Denormalization là gì?

Denormalization là việc cố tình đưa dữ liệu trùng lặp (intentional data duplication) hoặc gộp các bảng lại với nhau — đi ngược lại nguyên tắc chuẩn hóa — nhằm cải thiện hiệu năng đọc (read performance) hoặc đơn giản hóa truy vấn.

Lưu ý chữ "cố tình" (intentional). Đây là điểm mấu chốt phân biệt denormalization với một thiết kế tồi. Nếu bạn để dữ liệu trùng lặp vì không biết cách chuẩn hóa, đó là lỗi. Nếu bạn để dữ liệu trùng lặp vì đã cân nhắc kỹ và chấp nhận hậu quả, đó là denormalization. Cùng một kết quả trên giấy, nhưng khác nhau hoàn toàn về tư duy.

Tại sao chuẩn hóa đôi khi không đủ?

Chuẩn hóa được tối ưu cho ba mục tiêu chính:

  • Data integrity (toàn vẹn dữ liệu) — mỗi dữ kiện chỉ tồn tại ở một nơi, nên không có chuyện một thông tin có hai phiên bản mâu thuẫn nhau.
  • Tránh dư thừa (redundancy) — tiết kiệm dung lượng lưu trữ và tránh "dị thường cập nhật" (update anomaly).
  • Tối ưu cho ghi (write) — khi thêm/sửa/xóa, bạn chỉ động vào một chỗ duy nhất.
Vấn đề là: hệ thống thực tế không chỉ ghi, mà còn đọc rất nhiều. Và mỗi lần đọc dữ liệu nằm rải rác qua nhiều bảng, database phải thực hiện JOIN. Mỗi JOIN là một phép tính tốn kém. Khi bảng nhỏ thì không sao, nhưng khi bảng transactions của bạn có 200 triệu dòng, một truy vấn JOIN qua năm bảng có thể khiến server "thở dốc".

Denormalization về bản chất là đánh đổi chi phí ghi để lấy tốc độ đọc. Bạn chấp nhận tốn thêm dung lượng và chấp nhận phải duy trì tính nhất quán của dữ liệu trùng lặp, để đổi lấy việc câu truy vấn đọc chạy nhanh hơn nhiều lần.

Các kỹ thuật denormalization phổ biến

Trong thực tế, denormalization không phải một thao tác duy nhất mà là một nhóm kỹ thuật. Bạn nên nắm rõ từng loại để chọn đúng:

1. Pre-joined columns (cột sao chép từ bảng cha) — Thay vì JOIN sang bảng customers mỗi lần hiển thị đơn hàng, bạn lưu thẳng customer_name vào bảng orders. Đây là kiểu denormalization phổ biến nhất.

2. Derived / Aggregate columns (cột tính sẵn) — Thay vì đếm và cộng mỗi lần truy vấn, bạn lưu sẵn total_orderstotal_spent trong bảng customers, cập nhật mỗi khi có đơn mới. Người ta gọi đây là "materialized aggregate".

3. Repeating groups / arrays — Lưu một danh sách giá trị ngay trong một dòng (ví dụ một cột JSON chứa danh sách tag), thay vì tách ra bảng con. Kỹ thuật này gần gũi với mô hình NoSQL.

4. Table merging (gộp bảng) — Hai bảng có quan hệ một-một thường xuyên được truy vấn cùng nhau có thể gộp làm một để tránh JOIN.

5. Snapshot / historical copy — Sao chép giá trị tại một thời điểm để giữ lại lịch sử. Ví dụ lưu unit_price_at_purchase trong dòng chi tiết đơn hàng, vì giá sản phẩm có thể thay đổi sau này nhưng đơn hàng cũ phải giữ giá lúc mua.

Loại thứ năm này đặc biệt thú vị: nó trông giống denormalization nhưng thật ra là một yêu cầu nghiệp vụ chính đáng — bạn bắt buộc phải sao chép giá, vì giá tại thời điểm mua là một dữ kiện độc lập với giá hiện tại.

Tình huống thực tế

Tình huống 1 — Sàn thương mại điện tử và bài toán giá sản phẩm

Hãy tưởng tượng một sàn TMĐT kiểu Tiki hay Shopee tại Việt Nam. Trong mô hình chuẩn hóa, bảng order_items (chi tiết đơn hàng) chỉ lưu product_idquantity, còn giá thì tham chiếu sang bảng products.

Một ngày, đội marketing chạy chương trình Flash Sale 12.12: chiếc tai nghe giảm từ 1.200.000đ xuống 690.000đ. Hàng triệu đơn được đặt với giá 690.000đ. Hết khuyến mãi, giá trở lại 1.200.000đ.

Bây giờ phòng kế toán mở báo cáo doanh thu ngày 12.12. Nếu mô hình của bạn lấy giá bằng cách JOIN sang products, mọi đơn hàng cũ sẽ hiển thị giá 1.200.000đ — sai hoàn toàn so với số tiền khách thực trả. Doanh thu bị "thổi phồng" và sổ sách không khớp với cổng thanh toán.

Giải pháp: denormalize bằng cách thêm cột unit_price ngay trong order_items, ghi lại đúng giá tại thời điểm đặt. Đây là dữ liệu trùng lặp về mặt kỹ thuật, nhưng là yêu cầu nghiệp vụ tuyệt đối.

Bài học rút ra: Khi một giá trị cần "đóng băng" tại một thời điểm, denormalization không phải lựa chọn — nó là bắt buộc. Đừng để tư duy "phải chuẩn hóa tối đa" khiến bạn mất dữ liệu lịch sử.

Tình huống 2 — Ngân hàng số và màn hình lịch sử giao dịch

Một ngân hàng số tại Việt Nam (giả định, kiểu Timo hoặc Cake) có màn hình "Lịch sử giao dịch" mà mỗi khách hàng mở ra hàng chục lần mỗi ngày. Đây là tính năng được sử dụng nhiều nhất ứng dụng.

Mô hình chuẩn hóa của họ tách ra: transactions, accounts, merchants (đơn vị chấp nhận thanh toán), transaction_categories. Để hiển thị một dòng giao dịch đầy đủ ("Cà phê Highlands — Ăn uống — 65.000đ"), hệ thống phải JOIN bốn bảng. Với 5 triệu người dùng và bảng transactions lên đến hàng tỷ dòng, mỗi lần mở màn hình mất 3–4 giây. Khách hàng phàn nàn, tỉ lệ rời bỏ tăng.

Giải pháp: Đội kỹ thuật quyết định denormalize bảng transactions, thêm sẵn các cột merchant_name, category_name, account_alias. Những thông tin này được ghi vào ngay lúc giao dịch được tạo. Giờ đây màn hình lịch sử chỉ cần đọc một bảng duy nhất, thời gian phản hồi giảm xuống dưới 200ms.

Cái giá phải trả: Khi một merchant đổi tên (ví dụ "Highlands Coffee" đổi nhận diện thương hiệu), tên cũ vẫn nằm trong hàng triệu giao dịch lịch sử. Nhưng đội ngũ quyết định: với lịch sử giao dịch, hiển thị tên tại thời điểm giao dịch thực ra lại đúng hơn về mặt nghiệp vụ.

Bài học rút ra: Với những màn hình "đọc nhiều, ghi một lần rồi gần như không sửa", denormalization mang lại lợi ích khổng lồ về hiệu năng mà rủi ro nhất quán lại rất thấp.

Tình huống 3 — Khi denormalization phản tác dụng

Một startup giao đồ ăn cố denormalize bảng restaurants, sao chép restaurant_phonerestaurant_address vào mọi đơn hàng và mọi đánh giá để "đỡ phải JOIN". Nghe hợp lý.

Vấn đề nảy sinh khi nhà hàng đổi số điện thoại. Lập trình viên chỉ cập nhật bảng restaurants, quên mất rằng số cũ còn nằm rải rác ở năm bảng denormalized khác. Khách hàng gọi vào số cũ trong app, không liên lạc được, đơn bị hủy hàng loạt. Đây chính là update anomaly kinh điển.

Bài học rút ra: Đừng denormalize những dữ liệu thường xuyên thay đổicần luôn cập nhật theo bản gốc. Số điện thoại nhà hàng là dữ liệu "sống", phải luôn lấy bản mới nhất — nó hoàn toàn khác với "giá tại thời điểm mua" vốn là dữ liệu "chết". Phân biệt được dữ liệu sống và dữ liệu chết là chìa khóa.

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

Khi bạn đứng trước quyết định có nên denormalize hay không, hãy đi theo quy trình sau:

Bước 1 — Đo lường trước, đừng đoán. Đừng denormalize chỉ vì "linh cảm" rằng sẽ chậm. Hãy chạy truy vấn thật, xem EXPLAIN/execution plan, đo thời gian thực tế. Rất nhiều khi một index đặt đúng chỗ (xem trước Bài 23) đã giải quyết vấn đề mà không cần denormalize.

Bước 2 — Xác định đây là vấn đề đọc hay ghi. Denormalization chỉ giúp khi tải đọc (read) là điểm nghẽn. Nếu hệ thống nghẽn ở ghi, denormalization còn làm tệ hơn.

Bước 3 — Phân loại dữ liệu định sao chép: "sống" hay "chết". Dữ liệu bất biến theo thời gian (giá lúc mua, tên tại thời điểm giao dịch) → an toàn để denormalize. Dữ liệu thay đổi và cần đồng bộ (số điện thoại, trạng thái) → cân nhắc rất kỹ.

Bước 4 — Chọn kỹ thuật phù hợp. Cần tốc độ JOIN thì dùng pre-joined column. Cần tổng/đếm nhanh thì dùng aggregate column. Cần giữ lịch sử thì dùng snapshot.

Bước 5 — Thiết kế cơ chế đồng bộ. Đây là bước hay bị bỏ quên nhất. Nếu dữ liệu sao chép cần cập nhật, bạn phải quyết định: dùng trigger trong database, cập nhật ở tầng ứng dụng, hay chạy batch job định kỳ? Ghi rõ cơ chế này vào tài liệu.

Bước 6 — Ghi lại quyết định và cập nhật Data Dictionary. Đánh dấu rõ cột nào là denormalized, nguồn gốc từ đâu, đồng bộ bằng cách nào. Người đến sau cần biết cột customer_name trong orders là bản sao, không phải nguồn sự thật (source of truth).

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

Lỗi 1 — Denormalize quá sớm. Đây là dạng "tối ưu hóa non" (premature optimization). Nhiều người denormalize ngay từ đầu vì sợ chậm, để rồi gánh chi phí bảo trì cho một vấn đề chưa bao giờ xảy ra. Nguyên tắc: chuẩn hóa trước, denormalize sau, và chỉ khi có bằng chứng đo lường.

Lỗi 2 — Quên cơ chế đồng bộ. Bạn sao chép customer_name vào orders nhưng quên cập nhật nó khi khách đổi tên. Sau vài tháng, dữ liệu "trôi" (data drift) và không ai biết bản nào đúng.

Lỗi 3 — Không đánh dấu cột denormalized. Người mới vào dự án nhìn thấy hai cột customer_name ở hai bảng và sửa nhầm vào bản sao thay vì bản gốc.

Lỗi 4 — Denormalize dữ liệu thay đổi liên tục. Càng thay đổi nhiều, chi phí đồng bộ càng lớn và rủi ro càng cao.

Mẹo 1 — Cân nhắc materialized view trước. Nhiều DBMS (PostgreSQL, Oracle) hỗ trợ materialized view — cho bạn lợi ích đọc nhanh của denormalization mà vẫn giữ bảng gốc chuẩn hóa. Đôi khi đây là giải pháp "đẹp cả đôi đường".

Mẹo 2 — Tách báo cáo khỏi vận hành. Nếu nhu cầu denormalize chủ yếu phục vụ báo cáo, hãy nghĩ đến một cơ sở dữ liệu báo cáo riêng (sẽ học kỹ ở Bài 48) thay vì làm "bẩn" cơ sở dữ liệu vận hành.

Mẹo 3 — Luôn xác định "source of truth". Với mỗi dữ liệu trùng lặp, phải có một và chỉ một nơi được coi là nguồn chân lý. Mọi bản sao đều phải suy ra từ nguồn đó.

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

Bài 1 — Phân loại dữ liệu. Cho hệ thống đặt vé xem phim (kiểu CGV/Galaxy). Với mỗi dữ liệu sau, hãy quyết định nên giữ chuẩn hóa hay denormalize, và giải thích: (a) tên phim trong vé đã đặt, (b) số điện thoại rạp hiển thị trên app, (c) giá vé tại thời điểm đặt, (d) tổng số vé một khách đã mua.

Bài 2 — Thiết kế. Một app quản lý phòng gym có màn hình "Lịch sử check-in" mở rất thường xuyên, hiện đang JOIN bốn bảng (checkins, members, branches, staff). Hãy đề xuất phương án denormalize: chọn cột nào để sao chép, vào bảng nào, và mô tả cơ chế đồng bộ.

Bài 3 — Phản biện. Một đồng nghiệp đề xuất sao chép member_status (đang hoạt động / hết hạn) vào mọi bản ghi check-in. Hãy nêu ít nhất hai rủi ro của đề xuất này và đề xuất giải pháp thay thế.

Bài 4 — Đo lường. Mô tả các bước bạn sẽ làm để chứng minh rằng một truy vấn cần được denormalize, thay vì chỉ đoán. Liệt kê tối thiểu ba việc cụ thể.

Tóm tắt

Denormalization là việc cố tình đưa dữ liệu trùng lặp hoặc gộp bảng để đổi lấy tốc độ đọc và sự đơn giản trong truy vấn — đối lập có chủ đích với chuẩn hóa.

Những điểm cần khắc cốt ghi tâm:

  • Chuẩn hóa tối ưu cho toàn vẹn dữ liệu và ghi; denormalization tối ưu cho đọc. Mọi thứ đều là trade-off.
  • Hãy chuẩn hóa trước, denormalize sau, và chỉ khi có bằng chứng đo lường — đừng tối ưu hóa non.
  • Phân biệt dữ liệu "chết" (bất biến theo thời gian, an toàn để sao chép như giá lúc mua) và dữ liệu "sống" (cần đồng bộ, rủi ro cao như số điện thoại).
  • Mỗi quyết định denormalize phải đi kèm cơ chế đồng bộ rõ ràng và phải được ghi vào Data Dictionary.
  • Luôn xác định source of truth cho mọi dữ liệu trùng lặp.
  • Cân nhắc các giải pháp thay thế trước: index đúng chỗ, materialized view, hoặc cơ sở dữ liệu báo cáo riêng.
Khi bạn đã thành thạo cả chuẩn hóa lẫn denormalization, bạn không còn là người máy móc làm theo quy tắc nữa — bạn trở thành người ra quyết định thiết kế có cân nhắc. Và đó chính là giá trị mà một Business Analyst hay Data Modeler giỏi mang lại.