0

Google BigQuery - Bài học 10: Tư duy Denormalization – Tại sao trong BigQuery người ta lại thích gộp bảng thay vì tách bảng chuẩn hóa 3NF?

Chào anh em! Ở Bài học 9, chúng ta đã nắm chắc các kiểu dữ liệu cốt lõi trong GoogleSQL. Hôm nay, chúng ta sẽ bước vào một bài học mang tính "lật đổ" toàn bộ tư duy thiết kế cơ sở dữ liệu mà anh em từng được dạy khi mới học lập trình Backend: Tư duy Denormalization (Phi chuẩn hóa).

Nếu bạn mang nguyên vẹn cuốn sách giáo khoa cơ sở dữ liệu, các mô hình chuẩn hóa 3NF (Third Normal Form), cùng thói quen tách nhỏ bảng, tạo khóa ngoại (Foreign Key) từ MySQL hay PostgreSQL sang áp dụng cho BigQuery, hệ thống của bạn sớm muộn cũng sẽ "tắc thở" hoặc ngốn khoản tiền Cloud khổng lồ.

Tại sao trong thế giới OLAP của BigQuery, người ta lại thích gộp tất cả mọi thứ vào một bảng phẳng khổng lồ thay vì tách bạch rạch ròi? Hãy cùng mổ xẻ ngay bây giờ!

1. Thói quen 3NF trong thế giới OLTP: Tại sao ta phải tách bảng?

Khi thiết kế một hệ thống thương mại điện tử trên MySQL hoặc PostgreSQL (OLTP), quy tắc thiêng liêng nhất là Chuẩn hóa dữ liệu (Normalization), đặc biệt là đạt chuẩn 3NF:

  • Mục tiêu chính: Tránh dư thừa dữ liệu (Data Redundancy), loại bỏ các dị thường khi cập nhật (Update Anomaly, Insert Anomaly), và tiết kiệm từng kilobyte ổ cứng (vì ngày xưa ổ cứng cực kỳ đắt đỏ).

  • Cách thực hiện: Bạn xé nhỏ thông tin ra thành hàng chục bảng liên kết với nhau bằng khóa chính (Primary Key) và khóa ngoại (Foreign Key).

    • Bảng users (chứa thông tin khách hàng).

    • Bảng orders (chứa thông tin chung của đơn hàng, trỏ user_id sang bảng users).

    • Bảng order_items (chứa danh sách sản phẩm trong đơn, trỏ order_id sang bảng orders).

    • Bảng products (chứa thông tin sản phẩm).

Khi cần xem thông tin đơn hàng kèm tên khách hàng và sản phẩm, bạn viết một câu lệnh dài dằng dặc:

SQL

SELECT o.id, u.name, p.product_name, oi.quantity 
FROM orders o
JOIN users u ON o.user_id = u.id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;

Trên MySQL, với vài chục hay vài trăm nghìn dòng, cơ chế Index và Buffer Pool xử lý các phép JOIN này trong tích tắc. Nhưng khi đưa mô hình này lên BigQuery với quy mô hàng trăm triệu hoặc hàng tỷ dòng, thảm họa bắt đầu xuất hiện!

2. Ác mộng của phép JOIN trong hệ thống phân tán (Distributed JOIN Nightmare)

Tại sao các câu lệnh JOIN chằng chịt lại trở thành "kẻ thù" của Google BigQuery? Hãy nhớ lại kiến trúc mà chúng ta đã học ở Bài 4 (Mạng lưới Jupiter & Borg):

  1. Chi phí Shuffle qua mạng: Khi bạn JOIN bảng orders (100 triệu dòng) với bảng order_items (500 triệu dòng), các máy chủ tính toán (Slots) của Dremel không nằm trên cùng một con chip hay một ổ đĩa. Chúng được phân tán ra hàng ngàn máy chủ khác nhau.

  2. Để ghép các dòng lại với nhau theo khóa ngoại (order_id), hệ thống bắt buộc phải thực hiện Data Shuffle – tức là băm dữ liệu và bắn hàng trăm Gigabyte dữ liệu chéo qua lại giữa các máy chủ thông qua mạng cáp quang Jupiter.

  3. Nút thắt cổ chai phần cứng: Dù mạng Jupiter của Google có băng thông đạt mức Petabit/giây, việc truyền tải và đồng bộ hóa lượng dữ liệu khổng lồ trong một câu lệnh JOIN nhiều tầng vẫn tốn cực kỳ nhiều thời gian tính toán (Slot time) và làm giảm tốc độ phản hồi của báo cáo xuống mức tồi tệ.

Tóm lại: Trong thế giới OLTP, JOIN là biểu tượng của sự văn minh (chuẩn hóa). Còn trong thế giới OLAP phân tán, JOIN chính là cội nguồn của sự chậm chạp và tốn kém!

3. Triết lý Denormalization trong BigQuery: "Storage is Cheap, Network/Join is Expensive"

BigQuery lật ngược hoàn toàn tư duy của RDBMS bằng một triết lý thực dụng:

  • Ổ cứng (Storage) cực kỳ rẻ: Như đã học ở Bài 6, lưu trữ trên Colossus chỉ tốn khoảng $0.02/GB/tháng, lại được tự động giảm nửa giá sau 90 ngày. Việc để trùng lặp một vài thông tin (ví dụ: lặp lại tên khách hàng, địa chỉ, danh mục sản phẩm ở nhiều dòng đơn hàng khác nhau) không còn là tội ác nữa.

  • Tài nguyên mạng và tính toán (Network & Compute) mới là thứ đắt đỏ: Thay vì bắt hàng ngàn máy chủ phải JOIN bốn năm bảng lại với nhau mỗi khi có người chạy báo cáo, tại sao chúng ta không gộp (Denormalize) sẵn mọi thứ lại thành một bảng phẳng duy nhất ngay từ khâu nạp dữ liệu (ETL)?

Plaintext

[Mô hình 3NF Truyền thống (Nhiều bảng, JOIN liên tục)]
users <---> orders <---> order_items <---> products

                                VS

[Mô hình Denormalized trong BigQuery (Một bảng phẳng khổng lồ)]
orders_flattened (Chứa tất cả thông tin: user_name, user_email, product_name, category, quantity, price...)

Nhưng nếu gộp chung vào một bảng, dữ liệu bị lặp thì sao?

Đó là lúc sức mạnh của định dạng Capacitor (Bài 3) lên tiếng:

  • Vì Capacitor lưu trữ dữ liệu theo cột (Columnar), nên việc bạn lặp lại tên một danh mục sản phẩm hay tên thành phố của khách hàng hàng triệu lần trong một cột phẳng không làm tốn thêm bao nhiêu dung lượng ổ cứng nhờ thuật toán nén siêu đỉnh Dictionary Encoding và RLE (Run-Length Encoding).

  • Khi bạn cần chạy câu lệnh thống kê, Dremel chỉ cần mở đúng cột đó lên và quét qua một dải giá trị đã được nén khít rịt mà tuyệt đối không cần thực hiện một phép JOIN tốn kém nào qua mạng Jupiter!

4. Thực chiến: Thiết kế bảng Denormalized cho hệ thống E-commerce

Giả sử bạn đang xây dựng một bảng báo cáo doanh thu chi tiết cho sếp. Thay vì tạo 3 bảng tách biệt như MySQL, trong BigQuery, bạn sẽ thiết kế một bảng tổng hợp dạng phẳng (Flat Table) như sau:

order_id created_at customer_id customer_name customer_city product_id product_name category quantity unit_price total_price
1001 2026-10-18 C999 Nguyen Van A Ho Chi Minh P01 iPhone 15 Electronics 1 25,000,000 25,000,000
1001 2026-10-18 C999 Nguyen Van A Ho Chi Minh P02 Ốp lưng Accessories 2 200,000 400,000
1002 2026-10-18 C888 Tran Thi B Ha Noi P03 Macbook Air Electronics 1 30,000,000 30,000,000
  • Đặc điểm: Dữ liệu của khách hàng Nguyen Van A bị lặp lại ở hai dòng đơn hàng 1001. Nhưng đổi lại, khi bạn muốn chạy báo cáo doanh thu theo từng thành phố (customer_city) hoặc từng danh mục sản phẩm (category), bạn chỉ cần viết một câu lệnh cực kỳ đơn giản:

    SQL

    SELECT category, SUM(total_price) AS revenue
    FROM `my-project.mart_sales.flat_orders`
    WHERE created_at >= '2026-10-01'
    GROUP BY category;
    
    
  • Kết quả: Câu lệnh chạy cực kỳ chớp nhoáng, không cần JOIN, không cần Shuffle, chi phí quét dữ liệu tối thiểu và hoàn toàn không làm nghẽn hệ thống!

5. Khi nào KHÔNG NÊN Denormalization? (Biết điểm dừng để không lạm dụng)

Dù Denormalization là "chân ái" trong BigQuery, không có nghĩa là lúc nào bạn cũng nhồi nhét mọi thứ vào một bảng. Hãy tránh lạm dụng trong các trường hợp sau:

  1. Dữ liệu nguồn có tính biến động và cập nhật (UPDATE) liên tục: Nếu bảng users của bạn có thông tin số điện thoại, địa chỉ thay đổi hàng giây, và bạn đem đi gộp (Denormalize) vào 500 triệu dòng đơn hàng cũ, mỗi lần user đổi số điện thoại, bạn sẽ phải chạy câu lệnh UPDATE trên 500 triệu dòng đó. Đây là tội ác trên BigQuery, vì BigQuery là hệ thống lưu trữ bất biến (Immutable) – việc UPDATE liên tục sẽ làm vỡ các khối Capacitor và làm chậm toàn bộ hệ thống. (Quy tắc: Chỉ Denormalization với dữ liệu mang tính lịch sử, ít thay đổi hoặc dữ liệu dạng Append-only).

  2. Quan hệ 1-Nhiều cực kỳ rộng (Explosion / Cartesian Product): Nếu một đối tượng cha có tới hàng vạn đối tượng con, việc gộp phẳng theo kiểu truyền thống sẽ làm số lượng dòng tăng vọt theo cấp số nhân, dẫn đến bảng phình to bất thường. Đối với các cấu trúc lồng nhau (Nested data) như thế này, Google đã cung cấp một vũ khí tối thượng cao cấp hơn cả bảng phẳng truyền thống: Kiểu dữ liệu ARRAY và STRUCT!

Tổng kết Bài học 10

Tư duy Denormalization (Phi chuẩn hóa) là chìa khóa vàng giúp bạn chuyển đổi từ tư duy lập trình RDBMS sang Data Warehouse:

  1. Từ bỏ ám ảnh 3NF: Đừng sợ việc dư thừa dữ liệu hay lặp lại vài cột thông tin.

  2. Đánh đổi khôn ngoan: Hy sinh không gian lưu trữ (vốn rất rẻ trên Colossus) để đổi lấy hiệu năng truy vấn siêu tốc và triệt tiêu hoàn toàn các phép JOIN nặng nhọc qua mạng Jupiter.

  3. Xây dựng bảng phẳng (Flat Tables): Gom các chiều dữ liệu (Dimensions) vào chung với bảng sự kiện (Fact Table) để đội ngũ BI và báo cáo thỏa sức GROUP BY trực tiếp trong một nốt nhạc.

Nhưng nếu dữ liệu lồng nhau phức tạp (ví dụ: 1 đơn hàng chứa một mảng danh sách nhiều sản phẩm) mà bạn cứ ép buộc phải đập phẳng thành bảng 2 chiều truyền thống, dữ liệu sẽ bị lặp lại thảm hại. Vậy BigQuery có giải pháp nào vừa giữ được tính chất lồng nhau tự nhiên của JSON/Object, vừa đạt hiệu năng của lưu trữ dạng cột?

Câu trả lời sẽ có ngay trong bài học tiếp theo! Hẹn gặp lại anh em ở Bài học 11: Kiểu dữ liệu ARRAY và STRUCT – Vũ khí tối thượng thay thế quan hệ 1-N trong Data Warehouse!


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.