Google BigQuery - 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
Chào anh em! Ở Bài học 10, chúng ta đã làm quen với tư duy Denormalization (Phi chuẩn hóa) – nghệ thuật gom dữ liệu lại thành một bảng phẳng (Flat Table) để né tránh các phép JOIN tốn kém qua mạng Jupiter.
Tuy nhiên, như cuối bài trước chúng ta đã để ngỏ một câu hỏi: Nếu hệ thống của bạn có quan hệ 1-Nhiều (1-N) cực kỳ sâu (ví dụ: Một đơn hàng chứa một danh sách gồm hàng chục sản phẩm, một người dùng có danh sách nhiều thiết bị đăng nhập, hoặc một bài viết có hàng ngàn bình luận), việc ép buộc đập phẳng (flatten) dữ liệu theo kiểu truyền thống sẽ gây ra hiện tượng bùng nổ dòng (Row Explosion) – dữ liệu của bảng cha sẽ bị lặp lại thảm hại hàng chục lần, làm phồng rộp dung lượng bảng và làm rối tung các câu lệnh tính toán SUM, COUNT.
Vậy Google BigQuery giải quyết bài toán quan hệ 1-N như thế nào mà không cần tách bảng, không cần JOIN, nhưng vẫn giữ được tốc độ bàn thờ? Câu trả lời chính là bộ đôi vũ khí tối thượng: STRUCT và ARRAY.
Hôm nay, chúng ta sẽ mổ xẻ cấu trúc dữ liệu bán cấu trúc (Semi-structured data) đỉnh cao này!
1. Định nghĩa "cực dễ hiểu": STRUCT và ARRAY là gì?
Nếu anh em làm Backend (Golang, Node.js, Python, hay C++), hai khái niệm này quen thuộc không thể tả:
1.1. STRUCT (Structure) – "Cục" dữ liệu lồng nhau
-
Bản chất:
STRUCTgiống như một đối tượng (Objecttrong JavaScript/JSON) hoặc mộtStructtrong C/Go. Nó là một tập hợp các trường (fields) có tên và kiểu dữ liệu riêng biệt, được gói gọn lại bên trong một cột duy nhất. -
Ví dụ thực tế: Thay vì tách cột
customer_name,customer_email,customer_phonethành 3 cột riêng lẻ, bạn gom chúng lại thành một STRUCT tên làcustomer:SQL
STRUCT<name STRING, email STRING, phone STRING>
1.2. ARRAY (Mảng) – Danh sách các phần tử đồng nhất
-
Bản chất:
ARRAYgiống như một mảng (ArrayhoặcList) trong lập trình. Nó chứa danh sách gồm 0 hoặc nhiều phần tử có cùng một kiểu dữ liệu. -
Ví dụ thực tế: Một mảng các số nguyên lưu ID sản phẩm:
[101, 102, 103]hoặc một mảng chứa các chuỗi['COMPLETED', 'SHIPPED'].
1.3. Siêu vũ khí kết hợp: ARRAY<STRUCT<...>> (Mảng các cấu trúc)
Khi bạn kết hợp hai khái niệm này lại thành ARRAY<STRUCT>, bạn có trong tay một cấu trúc dữ liệu có khả năng biểu diễn toàn bộ quan hệ 1-Nhiều (1-N) ngay bên trong một hàng duy nhất của bảng cha!
Không cần bảng con. Không cần khóa ngoại (Foreign Key). Không cần JOIN. Mọi thứ gói gọn trong một dòng!
2. So sánh trực quan: Mô hình RDBMS truyền thống vs Mô hình Nested trong BigQuery
Hãy nhìn vào cách biểu diễn một đơn hàng gồm nhiều sản phẩm giữa hai thế giới:
Thế giới RDBMS (MySQL / PostgreSQL): Phải tách thành 2 bảng
-
Bảng
orders:order_id user_id order_date 1001 5001 2026-10-18 -
Bảng
order_items(Bảng con quan hệ 1-N):id order_id product_name quantity price 1 1001 iPhone 15 1 25,000,000 2 1001 Ốp lưng 2 200,000
Nhược điểm: Khi muốn lấy thông tin đơn hàng kèm theo danh sách sản phẩm, bắt buộc phải thực hiện phép JOIN giữa hai bảng, làm giảm hiệu suất khi quét dữ liệu lớn.
2. Thế Giới BigQuery (ARRAY & STRUCT): 1 Bảng Duy Nhất, Không Cần JOIN!
Trong BigQuery và các cơ sở dữ liệu hướng cột hiện đại, chúng ta có thể gom toàn bộ dữ liệu chi tiết vào một bảng duy nhất sử dụng cấu trúc lồng nhau (Nested & Repeated fields):
- Bảng
orders(Phi chuẩn hóa):order_id customer_name items ( ARRAY<STRUCT>)1001 Nguyen Van A [{"product_name": "iPhone 15", "quantity": 1, "price": 25000000}, {"product_name": "Ốp lưng", "quantity": 2, "price": 200000}]
Ưu điểm vượt trội: Loại bỏ hoàn toàn chi phí JOIN tốn kém, dữ liệu được lưu trữ liền kề nhau trên đĩa, giúp tăng tốc độ truy vấn gấp nhiều lần khi phân tích dữ liệu lớn.
Nhìn vào bảng BigQuery trên, bạn thấy điều gì? Toàn bộ danh sách sản phẩm của đơn hàng 1001 được nhét gọn ghẽ vào một cột duy nhất mang tên items.
3. Thực chiến GoogleSQL: Tạo bảng và Truy vấn dữ liệu ARRAY<STRUCT>
Hãy xắn tay áo lên và xem cách chúng ta định nghĩa DDL và viết câu lệnh truy vấn với kiểu dữ liệu này trong GoogleSQL.
Bước 1: Tạo bảng với cột lồng nhau (STRUCT và ARRAY)
SQL
CREATE TABLE `my-project.core_dwh.orders_nested` (
order_id INT64,
customer_name STRING,
-- Định nghĩa một ARRAY chứa các STRUCT mô tả sản phẩm trong đơn
items ARRAY<STRUCT<
product_id STRING,
product_name STRING,
quantity INT64,
price NUMERIC
>>,
created_at TIMESTAMP
);
Bước 2: Truy vấn dữ liệu lồng nhau bằng "Dot Notation" (Chấm điểm)
Vì cấu trúc đã được lồng sẵn, khi bạn muốn truy cập vào các trường bên trong STRUCT, bạn chỉ cần dùng dấu chấm (.) cực kỳ trực quan như khi code hướng đối tượng:
SQL
-- Lấy ra order_id và tên của sản phẩm đầu tiên trong mảng items (index bắt đầu từ 0 trong GoogleSQL)
SELECT
order_id,
customer_name,
items[OFFSET(0)].product_name AS first_product_name,
items[OFFSET(0)].price AS first_product_price
FROM `my-project.core_dwh.orders_nested`
WHERE order_id = 1001;
4. Bí mật bên dưới: Capacitor đã "cắt xẻ" ARRAY và STRUCT như thế nào?
Đến đây, một kỹ sư hệ thống tò mò sẽ đặt câu hỏi: "Nhét cả một cục danh sách phức tạp vào một cột thì khi quét dữ liệu, Capacitor có phải đọc toàn bộ cục JSON đó lên RAM không?"
Tuyệt đối KHÔNG! Hãy nhớ lại Bài học 3 (Định dạng Capacitor) và bài báo khoa học Dremel của Google:
-
Khi bạn lưu trữ
ARRAY<STRUCT>trong BigQuery, Capacitor không lưu chúng dưới dạng một chuỗi JSON thô phải đi parse bằng CPU mỗi lần đọc. -
Thay vào đó, Capacitor sử dụng hai thông số siêu việt mang tên Repetition Level và Definition Level để tự động bóc tách từng trường con bên trong
STRUCTvàARRAYra thành các cột vật lý độc lập trên ổ đĩa Colossus! -
Cột
product_nametrở thành một file riêng. Cộtpricethành một file riêng.
Kết quả là: Khi bạn viết câu lệnh tính tổng doanh thu toàn bộ sản phẩm:
SQL
-- Tính tổng tiền mà không cần quan tâm đến cấu trúc phức tạp bên trong
SELECT SUM(item.price * item.quantity)
FROM `my-project.core_dwh.orders_nested`,
UNNEST(items) AS item;
Dremel chỉ đọc đúng file vật lý chứa cột price và cột quantity, tốc độ nhanh hệt như một bảng phẳng truyền thống, nhưng dung lượng lưu trữ cực kỳ gọn gàng và không bị lặp dữ liệu lãng phí!
5. Phép thuật UNNEST: Biến mảng thành bảng phẳng khi cần thiết
Nhìn vào câu lệnh ở mục 4, anh em sẽ thấy xuất hiện từ khóa kỳ diệu UNNEST().
UNNEST chính là chìa khóa vạn năng để xử lý ARRAY. Bản chất của UNNEST là "trải phẳng" (flatten) một mảng các phần tử trở lại thành một tập hợp các dòng dữ liệu thông thường (giống như phép nhân chéo / Cross Join), giúp bạn có thể dễ dàng WHERE, GROUP BY, hoặc JOIN tiếp với các bảng khác.
Chúng ta sẽ dành trọn vẹn Bài học 12 tiếp theo để mổ xẻ tường tận mọi ngóc ngách của phép UNNEST và cách kết hợp nó đỉnh cao như thế nào!
Tổng kết Bài học 11
Kiểu dữ liệu ARRAY và STRUCT là minh chứng rõ nét cho sự thông minh trong thiết kế của Google BigQuery:
-
Thay thế quan hệ 1-N truyền thống: Cho phép gom toàn bộ dữ liệu con (danh sách sản phẩm, lịch sử thao tác) vào chung một dòng của bảng cha.
-
Hiệu năng phần cứng tối ưu: Không phải là JSON thô, Capacitor tự động bóc tách thành các cột vật lý riêng biệt, giúp truy vấn siêu tốc mà không tốn chi phí
JOIN. -
Cú pháp thân thiện: Dùng dấu chấm (
.) để truy xuất thuộc tính và hàmUNNEST()để trải phẳng khi cần tổng hợp dữ liệu.
Vậy cụ thể thì câu lệnh UNNEST() hoạt động ra sao dưới gầm máy, và làm thế nào để tránh những cái bẫy "bùng nổ dòng" (Row Explosion) khi UNNEST nhiều mảng cùng lúc?
Hẹn gặp lại anh em ở Bài học 12: Làm chủ UNNEST – Cách "trải phẳng" (Flatten) dữ liệu mảng và truy vấn siêu tốc!
All rights reserved